initial files
This commit is contained in:
84
src/models/authenticationModel.js
Normal file
84
src/models/authenticationModel.js
Normal file
@@ -0,0 +1,84 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
const Authentication = sequelize.define('Authentication', {
|
||||
ObjectGUID: {
|
||||
type: DataTypes.UUID,
|
||||
primaryKey: true,
|
||||
allowNull: false,
|
||||
},
|
||||
sAMAccountName: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
mail: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
givenName: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
sn: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
employeeID: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
department: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
streetAddress: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
userAccountControl_ID: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
authenticationType_ID: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
},
|
||||
telephoneNumber: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
physicalDeliveryOfficeName: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
distinguishedName: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
password: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true, // Passwort wird erst bei Erstanmeldung gesetzt
|
||||
},
|
||||
refreshtoken: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
active: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: true,
|
||||
},
|
||||
online: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: true,
|
||||
},
|
||||
}, {
|
||||
tableName: 'Authentication', // Tabellenname in der Datenbank
|
||||
timestamps: false, // Falls du keine createdAt/updatedAt Spalten hast
|
||||
})
|
||||
return Authentication;
|
||||
};
|
||||
51
src/models/eventlogModel.js
Normal file
51
src/models/eventlogModel.js
Normal file
@@ -0,0 +1,51 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
|
||||
async () => {
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
next(`database connection has been established successfully`);
|
||||
} catch(error) {
|
||||
next([`unable to connect to the database`, error]);
|
||||
}
|
||||
|
||||
}
|
||||
const EventLog = sequelize.define('EventLog', {
|
||||
ID: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
autoIncrement: true, // sorgt für Auto-Inkrement
|
||||
},
|
||||
Message: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
Trace: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
Level_ID: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
PluginName: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
Date: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
ObjectGUID: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
},
|
||||
}, {
|
||||
tableName: 'EventLog', // Tabellenname in der Datenbank
|
||||
timestamps: false, // Falls du keine createdAt/updatedAt Spalten hast
|
||||
})
|
||||
|
||||
return EventLog;
|
||||
};
|
||||
34
src/models/eventlogView.js
Normal file
34
src/models/eventlogView.js
Normal file
@@ -0,0 +1,34 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
const EventLogView = sequelize.define('EventLogView', {
|
||||
ID: { type: DataTypes.INTEGER, primaryKey: true },
|
||||
Message: { type: DataTypes.STRING },
|
||||
Trace: { type: DataTypes.STRING },
|
||||
Surname: { type: DataTypes.STRING },
|
||||
givenName: { type: DataTypes.STRING },
|
||||
Date: { type: DataTypes.DATE },
|
||||
department: { type: DataTypes.STRING },
|
||||
ClearTextUser: { type: DataTypes.STRING },
|
||||
ObjectTypDisplayName: { type: DataTypes.STRING },
|
||||
Level_ID: { type: DataTypes.INTEGER },
|
||||
LevelName: { type: DataTypes.STRING },
|
||||
LevelPriority: { type: DataTypes.INTEGER },
|
||||
LevelDisplayName: { type: DataTypes.STRING },
|
||||
PluginName: { type: DataTypes.STRING },
|
||||
ObjectGUID: { type: DataTypes.UUID },
|
||||
sAMAccountName: { type: DataTypes.STRING },
|
||||
mail: { type: DataTypes.STRING },
|
||||
Phone: { type: DataTypes.STRING },
|
||||
Office: { type: DataTypes.STRING },
|
||||
Adress: { type: DataTypes.STRING },
|
||||
authenticationType_ID: { type: DataTypes.INTEGER },
|
||||
TypeName: { type: DataTypes.STRING }
|
||||
}, {
|
||||
tableName: 'vEventLog', // dein SQL-View
|
||||
timestamps: false,
|
||||
freezeTableName: true
|
||||
});
|
||||
|
||||
return EventLogView;
|
||||
};
|
||||
108
src/models/integratedStartmenuItems.js
Normal file
108
src/models/integratedStartmenuItems.js
Normal file
@@ -0,0 +1,108 @@
|
||||
module.exports = ([
|
||||
{
|
||||
section: "System",
|
||||
name: 'Server',
|
||||
active: true,
|
||||
menu: {
|
||||
label: 'Server',
|
||||
items: [
|
||||
{
|
||||
label: 'Styles',
|
||||
view: 'styleconfig',
|
||||
icon: 'brush.png',
|
||||
permissions: [ 'Administration' ]
|
||||
},
|
||||
{
|
||||
label: 'Configs',
|
||||
view: 'serverconfig',
|
||||
icon: "app.png",
|
||||
permissions: [ 'Administration' ]
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
{
|
||||
section: 'System',
|
||||
name: 'EventLog',
|
||||
active: true,
|
||||
menu: {
|
||||
label: 'EventLog',
|
||||
items: [
|
||||
{
|
||||
label: 'EventLog',
|
||||
view: 'eventlog',
|
||||
defaultSize: { width: "1200px", height: "1200px" },
|
||||
icon: "eventlog.ico",
|
||||
permissions: [ 'Administration' ]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
section: 'System',
|
||||
name: 'Plugins',
|
||||
active: true,
|
||||
menu: {
|
||||
label: 'Plugins',
|
||||
items: [
|
||||
{
|
||||
label: 'Plugins',
|
||||
view: 'plugindashboard',
|
||||
defaultSize: { width: "1000px", height: "400px" },
|
||||
icon: "plugins.png",
|
||||
permissions: [ 'Administration' ]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
section: 'System',
|
||||
name: 'Info',
|
||||
active: true,
|
||||
menu: {
|
||||
label: 'Info',
|
||||
items: [
|
||||
{
|
||||
label: 'Info',
|
||||
view: 'serverinfo',
|
||||
defaultSize: { width: "900px", height: "500px" },
|
||||
icon: "serverinfo.png",
|
||||
permissions: [ 'Administration' ]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
section: "Benutzer",
|
||||
name: 'Einstellungen',
|
||||
active: true,
|
||||
menu: {
|
||||
label: 'Einstellungen',
|
||||
items: [
|
||||
{
|
||||
label: 'Einstellungen',
|
||||
view: 'usersettings',
|
||||
defaultSize: { width: "460px", height: "515px" },
|
||||
icon: "app.png",
|
||||
permissions: [ '*' ]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
section: "Benutzer",
|
||||
name: 'Hilfe',
|
||||
active: true,
|
||||
menu: {
|
||||
label: 'Hilfe',
|
||||
items: [
|
||||
{
|
||||
label: 'Hilfe',
|
||||
view: 'help',
|
||||
icon: "help.png",
|
||||
permissions: [ '*' ]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]);
|
||||
40
src/models/notifyTrayModel.js
Normal file
40
src/models/notifyTrayModel.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
|
||||
// // DB-Verbindung testen (optional)
|
||||
// (async () => {
|
||||
// try {
|
||||
// await sequelize.authenticate();
|
||||
// console.log(`Database connection established for NotifyTray`);
|
||||
// } catch (error) {
|
||||
// console.error(`Unable to connect to the database`, error);
|
||||
// }
|
||||
// })();
|
||||
|
||||
const NotifyTray = sequelize.define('NotifyTray', {
|
||||
ID: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
autoIncrement: true, // Identity
|
||||
},
|
||||
ObjectGUID: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
},
|
||||
NotifyTrayObject_ID: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true
|
||||
},
|
||||
SeenAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
}
|
||||
}, {
|
||||
tableName: 'NotifyTray',
|
||||
timestamps: false,
|
||||
});
|
||||
|
||||
return NotifyTray;
|
||||
};
|
||||
39
src/models/notifyTrayObjectsModel.js
Normal file
39
src/models/notifyTrayObjectsModel.js
Normal file
@@ -0,0 +1,39 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
const NotifyTrayObjects = sequelize.define('NotifyTrayObjects', {
|
||||
ID: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
autoIncrement: true, // manuell zu vergeben
|
||||
},
|
||||
PluginName: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true,
|
||||
},
|
||||
Message: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
},
|
||||
JSON: {
|
||||
type: DataTypes.TEXT, // große JSON-Strings
|
||||
allowNull: true,
|
||||
},
|
||||
ActionRequired: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
},
|
||||
CreatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
}
|
||||
}, {
|
||||
tableName: 'NotifyTrayObjects',
|
||||
timestamps: false,
|
||||
});
|
||||
|
||||
return NotifyTrayObjects;
|
||||
};
|
||||
70
src/models/notifyTrayView.js
Normal file
70
src/models/notifyTrayView.js
Normal file
@@ -0,0 +1,70 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
const NotifyTrayView = sequelize.define('vNotifyTray', {
|
||||
ID: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
},
|
||||
active: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: true,
|
||||
},
|
||||
online: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: true,
|
||||
},
|
||||
ObjectGUID: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
|
||||
sAMAccountName: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
givenName: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
sn: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
mail: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
PluginName: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true,
|
||||
},
|
||||
JSON: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
ActionRequired: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
},
|
||||
CreatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
Message: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
SeenAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
}
|
||||
}, {
|
||||
tableName: 'vNotifyTray', // 👈 WICHTIG: View statt Tabelle
|
||||
timestamps: false,
|
||||
});
|
||||
|
||||
return NotifyTrayView;
|
||||
};
|
||||
24
src/models/pluginModel.js
Normal file
24
src/models/pluginModel.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
const Plugin = sequelize.define('Plugin', {
|
||||
Name: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
primaryKey: true
|
||||
},
|
||||
Active: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true,
|
||||
},
|
||||
Version: {
|
||||
type: DataTypes.STRING(25),
|
||||
}
|
||||
}, {
|
||||
tableName: 'Plugins', // Tabellenname in der Datenbank
|
||||
timestamps: false, // Falls du keine createdAt/updatedAt Spalten hast
|
||||
});
|
||||
|
||||
return Plugin;
|
||||
};
|
||||
58
src/models/releasenotes.json
Normal file
58
src/models/releasenotes.json
Normal file
@@ -0,0 +1,58 @@
|
||||
[
|
||||
{
|
||||
"sAMAccountName": "manuel.sowada",
|
||||
"plugin": "System",
|
||||
"datetime": "2026-02-11",
|
||||
"value": "HTML-Tabellen werden durch virtuelles Dataset abgebildet",
|
||||
"finish": true
|
||||
},
|
||||
{
|
||||
"sAMAccountName": "manuel.sowada",
|
||||
"plugin": "System",
|
||||
"datetime": "2026-04-14",
|
||||
"value": "Bubble-Notify eingefügt",
|
||||
"finish": true
|
||||
},
|
||||
{
|
||||
"sAMAccountName": "manuel.sowada",
|
||||
"plugin": "System",
|
||||
"datetime": "2026-04-16",
|
||||
"value": "Window drag 'n drop bugfixes",
|
||||
"finish": true
|
||||
},
|
||||
{
|
||||
"sAMAccountName": "manuel.sowada",
|
||||
"plugin": "System",
|
||||
"datetime": "2026-04-17",
|
||||
"value": "OS-Logik und -Design Bugfixes + mobile-responsive-design",
|
||||
"finish": true
|
||||
},
|
||||
{
|
||||
"sAMAccountName": "manuel.sowada",
|
||||
"plugin": "System",
|
||||
"datetime": "2026-04-18",
|
||||
"value": "Speichern der Window-Payloads pro Benutzer, divers Bugfixes und Verbesserungen in der OS-Logik",
|
||||
"finish": true
|
||||
},
|
||||
{
|
||||
"sAMAccountName": "manuel.sowada",
|
||||
"plugin": "System",
|
||||
"datetime": "2026-04-19",
|
||||
"value": "PluginManager erweitert, Hilfe in Tabs umbgebaut, diverse Design-Bugfixes",
|
||||
"finish": true
|
||||
},
|
||||
{
|
||||
"sAMAccountName": "manuel.sowada",
|
||||
"plugin": "System",
|
||||
"datetime": "2026-04-20",
|
||||
"value": "OS-Logik update: fetch ersetzt socket.io bei open_window und open_view",
|
||||
"finish": true
|
||||
},
|
||||
{
|
||||
"sAMAccountName": "manuel.sowada",
|
||||
"plugin": "System",
|
||||
"datetime": "2026-04-21",
|
||||
"value": "Plugin-System + JSONtree bugfixes",
|
||||
"finish": true
|
||||
}
|
||||
]
|
||||
175
src/routes/adminRoutes.js
Normal file
175
src/routes/adminRoutes.js
Normal file
@@ -0,0 +1,175 @@
|
||||
const { exec } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { service } = require('@root/server.js');
|
||||
|
||||
|
||||
const configurationFile = path.join(require('@root/server.js').path.source, 'models', 'configuration.json');
|
||||
const stylesheetFile = path.join(require('@root/server.js').path.source, 'models', 'stylesheet.json');
|
||||
const serverInfoFile = path.join(require('@root/server.js').path.root, 'package.json');
|
||||
|
||||
|
||||
module.exports = {
|
||||
route(app, authenticationManager, pluginManager, eventManager, socketManager, activeDirectoryManager, stylesheetJson) {
|
||||
// JSON configuration abrufen
|
||||
app.post('/api/getConfig', (req, res) => {
|
||||
fs.readFile(configurationFile, 'utf8', (err, data) => {
|
||||
if (err) return res.status(500).send(err);
|
||||
res.status(200).send(JSON.parse(data));
|
||||
});
|
||||
});
|
||||
|
||||
// JSON configuration speichern
|
||||
app.post('/config', (req, res) => {
|
||||
fs.writeFile(configurationFile, JSON.stringify(req.body, null, 2), (err) => {
|
||||
if (err) return res.status(500).send(err);
|
||||
res.status(200).send({ status: 'ok' });
|
||||
});
|
||||
});
|
||||
|
||||
// JSON stylesheet abrufen
|
||||
app.post('/api/getStyles', (req, res) => {
|
||||
fs.readFile(stylesheetFile, 'utf8', (err, data) => {
|
||||
if (err) return res.status(500).send(err);
|
||||
res.status(200).send(JSON.parse(data));
|
||||
});
|
||||
});
|
||||
|
||||
// JSON stylesheet speichern
|
||||
app.post('/style', (req, res) => {
|
||||
fs.writeFile(stylesheetFile, JSON.stringify(req.body, null, 2), (err) => {
|
||||
if (err) return res.status(500).send(err);
|
||||
res.status(200).send({ status: 'ok' });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// JSON package.json abrufen
|
||||
app.post('/api/getServerInfo', (req, res) => {
|
||||
fs.readFile(serverInfoFile, 'utf8', (err, data) => {
|
||||
if (err) { return res.status(500).send(err); }
|
||||
res.status(200).send({ package: JSON.parse(data), pid: process.pid, releaseNotes: global.json.releaseNotes.live });
|
||||
});
|
||||
});
|
||||
|
||||
// JSON package.json speichern
|
||||
app.post('/serverinfo', (req, res) => {
|
||||
fs.writeFile(serverInfoFile, JSON.stringify(req.body, null, 2), (err) => {
|
||||
if (err) return res.status(500).send(err);
|
||||
res.status(200).send({ status: 'ok' });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
app.post('/api/eventlog/clearlog', (req, res) => {
|
||||
eventManager.clear();
|
||||
res.status(200).send({ status: 'ok' })
|
||||
})
|
||||
|
||||
app.post('/api/eventlog/getLogs', async (req, res) => {
|
||||
res.status(200).json(await service.get('eventManager').getAllEventLogs());
|
||||
})
|
||||
|
||||
|
||||
app.post('/api/shutdown', (req, res) => {
|
||||
exec(`kill -9 ${process.pid}`, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
service.get('eventManager').write(req.cookies.ObjectGUID, 4, null, error.message);
|
||||
return res.status(500).send({ status: 'error', message: error.message });
|
||||
}
|
||||
service.get('eventManager').write(req.cookies.ObjectGUID, 0, null, `Server heruntergefahren`);
|
||||
res.status(200).send({ status: 'ok' });
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/restart', (req, res) => {
|
||||
service.get('eventManager').write(req.cookies.ObjectGUID, 2, null, `Der Neustart ist noch nicht implementiert`);
|
||||
res.status(200).send({ status: 'ok' });
|
||||
// exec(`kill -9 ${process.pid}`, (error, stdout, stderr) => {
|
||||
// if (error) {
|
||||
// service.get('eventManager').write(req.cookies.ObjectGUID, 4, null, error.message);
|
||||
// return res.status(500).send({ status: 'error', message: error.message });
|
||||
// }
|
||||
// service.get('eventManager').write(req.cookies.ObjectGUID, 0, null, `Server neu gestartet`);
|
||||
// res.status(200).send({ status: 'ok' });
|
||||
// });
|
||||
});
|
||||
|
||||
|
||||
app.post('/api/plugins/activation', async (req, res) => {
|
||||
const { name, state } = req.body;
|
||||
let result = null;
|
||||
if(state) {
|
||||
result = await pluginManager.load(name, true);
|
||||
} else {
|
||||
result = await pluginManager.unload(name);
|
||||
}
|
||||
console.log(result)
|
||||
// result = { ...result, authorized: result.metadata.permissions.some(async permission => { await activeDirectoryManager.getGroup(permission) != null && await activeDirectoryManager.isUserMemberOfRecursive(req.cookies.sAMAccountName, permission)}) }
|
||||
|
||||
eventManager.write(null, result.levelId, name, result.message);
|
||||
// socketManager.broadcast('admin', 'plugin_status', result);
|
||||
socketManager.broadcast('/', 'plugin_status', result);
|
||||
res.status(200).json(result);
|
||||
});
|
||||
|
||||
app.post('/api/plugins/getAll', async (req, res) => {
|
||||
try {
|
||||
const plugins = await pluginManager.getStatus();
|
||||
res.status(200).json(plugins);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
app.post('/api/plugins/:name/update', async (req, res) => {
|
||||
const { name } = req.params;
|
||||
try {
|
||||
const { updates } = req.body;
|
||||
const result = await pluginManager.update(name, updates);
|
||||
// result = { ...result, authorized: result.metadata.permissions.some(async permission => { await activeDirectoryManager.getGroup(permission) != null && await activeDirectoryManager.isUserMemberOfRecursive(req.cookies.sAMAccountName, permission)}) }
|
||||
|
||||
service.get('eventManager').writeLog(req.cookies.ObjectGUID, result.levelId, name, result.message);
|
||||
// socketManager.broadcast('admin', 'plugin_status', result);
|
||||
// socketManager.broadcast('/', 'plugin_status', result);
|
||||
res.status(200).json(result);
|
||||
} catch (error) {
|
||||
service.get('eventManager').write(req.cookies.ObjectGUID, 4, name, `Fehler beim Aktualisieren des Plugins: ${error}`);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/plugins/:name/rename', async (req, res) => {
|
||||
const { name } = req.params;
|
||||
const { newName } = req.body;
|
||||
const result = await service.get('pluginManager').rename(name, newName);
|
||||
|
||||
// const result = { levelId: 0, pluginName: name, message: `Plugin erstellt` };
|
||||
// await pluginManager.create(name);
|
||||
// res.status(200).json(result);
|
||||
|
||||
eventManager.writeLog(null, result.levelId, name, result.message);
|
||||
// socketManager.broadcast('admin', 'plugin_status', result);
|
||||
});
|
||||
|
||||
app.post('/api/plugins/:name/create', async (req, res) => {
|
||||
const { name } = req.params;
|
||||
const result = { levelId: 0, pluginName: name, message: `Plugin erstellt` };
|
||||
await pluginManager.create(name);
|
||||
|
||||
eventManager.writeLog(null, result.levelId, name, result.message);
|
||||
res.status(200).json(result);
|
||||
// socketManager.broadcast('admin', 'plugin_status', result);
|
||||
});
|
||||
|
||||
app.post('/admin/plugins/:name/delete', async (req, res) => {
|
||||
const { name } = req.params;
|
||||
const result = { status: 'delete', pluginName: name, levelId: 0, message: `Plugin ${name} gelöscht` }; //await pluginManager.delete(name);
|
||||
res.status(200).json(result);
|
||||
|
||||
eventManager.write(null, result.levelId, name, result.message);
|
||||
socketManager.broadcast('admin', 'plugin_status', result);
|
||||
});
|
||||
}
|
||||
};
|
||||
125
src/routes/indexRoutes.js
Normal file
125
src/routes/indexRoutes.js
Normal file
@@ -0,0 +1,125 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const { renderWindow } = require('@services/renderWindow.js');
|
||||
const { service } = require('@root/server.js');
|
||||
const { File: HotReload } = require(`@services/hotReload.js`);
|
||||
|
||||
const { doesNotReject } = require('assert');
|
||||
// let startMenuItemContext = require('@models/integratedStartmenuItems.js')
|
||||
|
||||
|
||||
module.exports = {
|
||||
route: function(app, service) {
|
||||
app.get('/', service.get('authenticationManager').authenticate(), async (req, res) => {
|
||||
const startMenuItems = await global.startMenuItems(
|
||||
app,
|
||||
req.cookies.sAMAccountName,
|
||||
service
|
||||
);
|
||||
res.render('desktop', { layout: 'default', startMenuItems: startMenuItems });
|
||||
});
|
||||
|
||||
|
||||
app.post('/api/open_app', (req, res) => {
|
||||
const { name, view, viewLabel, location, size, state, zIndex } = req.body;
|
||||
|
||||
const pluginPath = path.join(global.path.plugins, name, 'plugin.json');
|
||||
|
||||
let context = fs.existsSync(pluginPath)
|
||||
? service.get('fileSystemManager').loadJSON(pluginPath)
|
||||
: (global.json.startMenuItems.live).find(item => item.name == name);
|
||||
|
||||
context.defaultSize =
|
||||
context.menu.items.find(item => item.label == viewLabel)?.defaultSize ||
|
||||
{ width: 800, height: 600 };
|
||||
|
||||
delete context.config;
|
||||
res.json({ name, view, viewLabel, context, location, size, state, zIndex });
|
||||
});
|
||||
|
||||
|
||||
|
||||
app.get('/api/NotifyTray/getTrays', async (req, res) => {
|
||||
const objectGuid = req.cookies.ObjectGUID;
|
||||
// console.log(await service.get('notifyTray').getOpenNotifications(objectGuid))
|
||||
res.status(200).json(await service.get('notifyTray').getOpenNotifications(objectGuid));
|
||||
})
|
||||
|
||||
app.post('/api/NotifyTray/markAsSeen', async (req, res) => {
|
||||
const objectGuid = req.cookies.ObjectGUID;
|
||||
const notificationId = req.body.id;
|
||||
const notificationValue = req.body.value;
|
||||
await service.get('notifyTray').markAsSeen(objectGuid, notificationId, notificationValue);
|
||||
res.status(204).send();
|
||||
})
|
||||
|
||||
app.post('/api/Plugins/loadScripts', service.get('authenticationManager').authenticate(), async(req, res) => {
|
||||
const scripts = service.get('pluginManager').getStatus().map(plugin => {
|
||||
|
||||
const exists = service.get('fileSystemManager').exists(path.join(plugin.pluginPath, 'public', 'javascript', 'main.js'))
|
||||
if (exists && exists.status) {
|
||||
return path.join('/', plugin.name, 'javascript', 'main.js');
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(script => script !== null);
|
||||
res.status(200).send(scripts);
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
app.post('/window/EventLog/eventlog', async (req, res) => {
|
||||
await renderWindow(app, 'eventlog.hbs', { }, { }, res)
|
||||
});
|
||||
|
||||
app.post('/window/Server/styleconfig', async (req, res) => {
|
||||
await renderWindow(app, 'styleconfig.hbs', { }, { }, res)
|
||||
});
|
||||
|
||||
app.post('/window/Plugins/plugindashboard', async (req, res) => {
|
||||
await renderWindow(app, 'plugindashboard.hbs', { }, { }, res)
|
||||
});
|
||||
|
||||
app.post('/window/Info/serverinfo', async (req, res) => {
|
||||
await renderWindow(app, 'serverinfo.hbs', { }, { }, res)
|
||||
});
|
||||
|
||||
app.post('/window/Server/serverconfig', async (req, res) => {
|
||||
await renderWindow(app, 'serverconfig.hbs', { }, { }, res)
|
||||
});
|
||||
|
||||
app.post('/window/Einstellungen/usersettings', async (req, res) => {
|
||||
await renderWindow(app, 'usersettings.hbs', { }, { }, res)
|
||||
});
|
||||
|
||||
app.post('/window/Hilfe/help', async (req, res) => {
|
||||
await renderWindow(app, 'help.hbs', { }, { }, res)
|
||||
});
|
||||
|
||||
app.get('/api/help/getTabs', async (req, res) => {
|
||||
const tabNames = (await startMenuItems(app, req.cookies.sAMAccountName))
|
||||
.filter(plugin => plugin.active && plugin.menu.items.some(i => i.authorized))
|
||||
.map(plugin => ({ name: plugin.menu.label }));
|
||||
res.status(200).json(tabNames);
|
||||
});
|
||||
|
||||
app.post('/api/help/getHelp', async (req, res) => {
|
||||
const { name } = req.body;
|
||||
const props = (await startMenuItems(app, req.cookies.sAMAccountName))
|
||||
.filter(plugin => plugin.name === name && plugin.active && plugin.menu.items.some(i => i.authorized))
|
||||
.map(async plugin => (
|
||||
{
|
||||
name: plugin.menu.label,
|
||||
version: plugin.version,
|
||||
description: plugin.description,
|
||||
html: await fs.promises.readFile(plugin.section === 'Plugin' ? path.join(plugin.pluginPath, 'docs', 'help.html') : path.join(app.locals.path.public, 'views', 'help', plugin.name + '.html'), 'utf-8')
|
||||
}
|
||||
));
|
||||
res.status(200).send(await props[0]);
|
||||
});
|
||||
}
|
||||
};
|
||||
79
src/routes/loginRoutes.js
Normal file
79
src/routes/loginRoutes.js
Normal file
@@ -0,0 +1,79 @@
|
||||
const { verify } = require("jsonwebtoken");
|
||||
|
||||
module.exports = {
|
||||
route(app, authenticationManager, socketManager, eventManager) {
|
||||
app.get(`/login`, (req, res) => {
|
||||
res.render(`login`, { layout: 'default' });
|
||||
})
|
||||
|
||||
app.post('/login', async (req, res) => {
|
||||
const { sAMAccountName, password } = req.body;
|
||||
const userModel = await authenticationManager.Authentication.findOne({
|
||||
where: { sAMAccountName: sAMAccountName }, attributes: ['ObjectGUID'],
|
||||
raw: true
|
||||
});
|
||||
const objectGuid = userModel !== null ? userModel.ObjectGUID : sAMAccountName;
|
||||
try {
|
||||
// set safety cookies
|
||||
res.cookie('sAMAccountName', sAMAccountName, {
|
||||
httpOnly: false,
|
||||
secure: true,
|
||||
sameSite: 'Strict',
|
||||
maxAge: 1000 * 60 * 60 * 24 * 365
|
||||
})
|
||||
|
||||
res.cookie('ObjectGUID', objectGuid, {
|
||||
httpOnly: false,
|
||||
secure: true,
|
||||
sameSite: 'Strict',
|
||||
maxAge: 1000 * 60 * 60 * 24 * 365
|
||||
})
|
||||
const login = await authenticationManager.login(sAMAccountName, password);
|
||||
|
||||
eventManager.writeLog(objectGuid, login.levelId, null, login.message);
|
||||
res.status(login.levelId == 0 ? 200 : 401).json(login);
|
||||
} catch (err) {
|
||||
res.status(500).json(login);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Geschützte Route
|
||||
app.get('/me', authenticationManager.authenticate(), (req, res) => {
|
||||
res.json(JSON.stringify({
|
||||
user: {
|
||||
name: req.user
|
||||
}
|
||||
}, null, 4));
|
||||
});
|
||||
|
||||
app.post('/checkLoginName', async (req, res) => {
|
||||
const { sAMAccountName } = req.body;
|
||||
|
||||
const userExists = await authenticationManager.Authentication.findOne({ where: { sAMAccountName: sAMAccountName } });
|
||||
const auth = { objectGuid: userExists != null ? userExists.ObjectGUID : sAMAccountName, sAMAccountName: sAMAccountName };
|
||||
res.status(userExists ? 200 : 404).json({ exists: userExists != null });
|
||||
});
|
||||
|
||||
app.get('/verifying', async (req, res, next) => {
|
||||
const verify = await authenticationManager.verifyUserToken();
|
||||
eventManager.write(req.user.objectGuid, verify.levelId, null, verify.message);
|
||||
next();
|
||||
});
|
||||
|
||||
// Logout
|
||||
app.post('/logout', authenticationManager.authenticate(), async (req, res) => {
|
||||
const logout = await authenticationManager.logout(req.user.sAMAccountName);
|
||||
|
||||
socketManager.sendTo('/', req.user.objectGuid, 'login_status', { levelId: logout.levelId, message: logout.message } )
|
||||
eventManager.write(req.user.objectGuid, logout.levelId, null, logout.message);
|
||||
|
||||
res.clearCookie('sAMAccountName');
|
||||
res.clearCookie('ObjectGUID');
|
||||
|
||||
setTimeout(() => res.render('login', { layout: false, title: app.locals.configuration.server.name }), 3000);
|
||||
// res.json({ message: 'Logout erfolgreich' });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
258
src/services/activeDirectoryManager.js
Normal file
258
src/services/activeDirectoryManager.js
Normal file
@@ -0,0 +1,258 @@
|
||||
const ActiveDirectory = require('activedirectory2');
|
||||
|
||||
class ActiveDirectoryManager {
|
||||
constructor({
|
||||
url,
|
||||
baseDN,
|
||||
username,
|
||||
password,
|
||||
userAttributes,
|
||||
groupAttributes,
|
||||
computerAttributes
|
||||
}) {
|
||||
this.ad = new ActiveDirectory({
|
||||
url,
|
||||
baseDN,
|
||||
username,
|
||||
password,
|
||||
attributes: {
|
||||
user: userAttributes,
|
||||
group: groupAttributes,
|
||||
computer: computerAttributes
|
||||
}
|
||||
});
|
||||
|
||||
this.userAttributes = userAttributes;
|
||||
this.groupAttributes = groupAttributes;
|
||||
this.computerAttributes = computerAttributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* -----------------------------------------------------
|
||||
* INTERNAL GENERIC LDAP SEARCH
|
||||
* -----------------------------------------------------
|
||||
*/
|
||||
async ldapSearch(options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.ad.find(options, (err, result) => {
|
||||
if (err) return reject(err);
|
||||
resolve(result || {});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* -----------------------------------------------------
|
||||
* USER FUNCTIONS
|
||||
* -----------------------------------------------------
|
||||
*/
|
||||
|
||||
async getUser(username, attributes = this.userAttributes) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.ad.findUser({ attributes }, username, (err, user) => {
|
||||
if (err) return reject(err);
|
||||
resolve(user || null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async getUserDN(username) {
|
||||
const user = await this.getUser(username);
|
||||
return user?.dn || null;
|
||||
}
|
||||
|
||||
async findUsers(query, attributes = this.userAttributes) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const filter = `(&(objectClass=user)(|(cn=${query})(sAMAccountName=${query})(mail=${query})(displayName=${query})))`;
|
||||
|
||||
this.ad.findUsers({ filter, attributes }, (err, users) => {
|
||||
if (err) return reject(err);
|
||||
resolve(users || []);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* -----------------------------------------------------
|
||||
* GROUP FUNCTIONS
|
||||
* -----------------------------------------------------
|
||||
*/
|
||||
|
||||
async getGroup(groupName, attributes = this.groupAttributes) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.ad.findGroup({ attributes }, groupName, (err, group) => {
|
||||
if (err) return reject(err);
|
||||
resolve(group || null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async findGroups(query, attributes = this.groupAttributes) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const filter = `(&(objectClass=group)(cn=${query}))`;
|
||||
|
||||
this.ad.findGroups({ filter, attributes }, (err, groups) => {
|
||||
if (err) return reject(err);
|
||||
resolve(groups || []);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* -----------------------------------------------------
|
||||
* COMPUTER / OU FUNCTIONS 🖥️
|
||||
* -----------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Einzelnen Computer holen
|
||||
*/
|
||||
async getComputer(name, attributes = this.computerAttributes) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const filter = `(&(objectClass=computer)(|(cn=${name})(dNSHostName=${name})))`;
|
||||
|
||||
this.ad.find({ filter, attributes }, (err, result) => {
|
||||
if (err) return reject(err);
|
||||
resolve(result?.other?.[0] || null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Alle Computer
|
||||
*/
|
||||
async getComputers(attributes = this.computerAttributes) {
|
||||
const options = {
|
||||
baseDN: this.ad.baseDN,
|
||||
filter: '(objectClass=computer)',
|
||||
attributes
|
||||
};
|
||||
|
||||
const result = await this.ldapSearch(options);
|
||||
return result.other || [];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Alle Computer aus einer OU holen
|
||||
*/
|
||||
async getComputersFromOU(ouDn, attributes = this.computerAttributes) {
|
||||
const options = {
|
||||
baseDN: ouDn,
|
||||
filter: '(objectClass=computer)',
|
||||
attributes
|
||||
};
|
||||
|
||||
const result = await this.ldapSearch(options);
|
||||
return result.other || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Computer suchen (Wildcard möglich)
|
||||
* Beispiele: "PC-*", "*LAPTOP*", "SRV01"
|
||||
*/
|
||||
async findComputers(query, attributes = this.computerAttributes) {
|
||||
const filter = `(&(objectClass=computer)(|(cn=${query})(dNSHostName=${query})))`;
|
||||
|
||||
const result = await this.ldapSearch({
|
||||
filter,
|
||||
attributes
|
||||
});
|
||||
return result.other || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* -----------------------------------------------------
|
||||
* GROUP MEMBERSHIP (DIRECT & RECURSIVE)
|
||||
* -----------------------------------------------------
|
||||
*/
|
||||
|
||||
async isUserMemberOfDirect(username, groupName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.ad.isUserMemberOf(username, groupName, (err, isMember) => {
|
||||
if (err) return reject(err);
|
||||
resolve(isMember);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async isUserMemberOfRecursive(username, groupName, visited = new Set()) {
|
||||
const key = groupName.toLowerCase();
|
||||
if (visited.has(key)) return false;
|
||||
visited.add(key);
|
||||
|
||||
const direct = await this.isUserMemberOfDirect(username, groupName);
|
||||
if (direct) return true;
|
||||
|
||||
const group = await this.getGroup(groupName);
|
||||
if (!group || !Array.isArray(group.member)) return false;
|
||||
|
||||
for (const dn of group.member) {
|
||||
const match = dn.match(/CN=([^,]+)/i);
|
||||
if (!match) continue;
|
||||
|
||||
const subGroupName = match[1];
|
||||
const found = await this.isUserMemberOfRecursive(username, subGroupName, visited);
|
||||
if (found) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async getGroupSubgroups(groupName, visited = new Set()) {
|
||||
const key = groupName.toLowerCase();
|
||||
if (visited.has(key)) return [];
|
||||
|
||||
visited.add(key);
|
||||
|
||||
const group = await this.getGroup(groupName);
|
||||
if (!group || !Array.isArray(group.member)) return [];
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const memberDN of group.member) {
|
||||
const match = memberDN.match(/CN=([^,]+)/i);
|
||||
if (!match) continue;
|
||||
|
||||
const subGroupName = match[1];
|
||||
const sub = await this.getGroup(subGroupName).catch(() => null);
|
||||
if (!sub) continue;
|
||||
|
||||
results.push(sub);
|
||||
results.push(...await this.getGroupSubgroups(subGroupName, visited));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async getGroupRecursive(groupName, visited = new Set()) {
|
||||
const key = groupName.toLowerCase();
|
||||
if (visited.has(key)) return null;
|
||||
|
||||
visited.add(key);
|
||||
|
||||
const group = await this.getGroup(groupName);
|
||||
if (!group) return null;
|
||||
|
||||
const result = {
|
||||
...group,
|
||||
subgroups: []
|
||||
};
|
||||
|
||||
if (!Array.isArray(group.member)) return result;
|
||||
|
||||
for (const memberDN of group.member) {
|
||||
const match = memberDN.match(/CN=([^,]+)/i);
|
||||
if (!match) continue;
|
||||
|
||||
const subGroupName = match[1];
|
||||
const subTree = await this.getGroupRecursive(subGroupName, visited);
|
||||
if (subTree) result.subgroups.push(subTree);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ActiveDirectoryManager;
|
||||
172
src/services/authenticationManager.js
Normal file
172
src/services/authenticationManager.js
Normal file
@@ -0,0 +1,172 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const bcrypt = require('bcryptjs');
|
||||
|
||||
let { levelId, message } = '';
|
||||
|
||||
/**
|
||||
* Authentication class for login method, token validation and password setting
|
||||
*/
|
||||
class AuthenticationManager {
|
||||
/**
|
||||
*
|
||||
* @param {object} model - Use the authentication database model for interact with the database
|
||||
* @param {string} secretKey - Defines the server secret for token validation
|
||||
*/
|
||||
constructor(model, secretKey, eventManager) {
|
||||
this.eventManager = eventManager;
|
||||
|
||||
// if (!model) throw new Error('Sequelize Model wird benötigt');
|
||||
// if (!secretKey) throw new Error('Secret Key wird benötigt');
|
||||
|
||||
this.Authentication = model;
|
||||
this.SECRET_KEY = secretKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set or reset password of user
|
||||
* @param {string} sAMAccountName - Windows account name
|
||||
* @param {string} password - Set the new password
|
||||
*/
|
||||
async setPassword(sAMAccountName, password) {
|
||||
const user = await this.Authentication.findOne({ where: { sAMAccountName } });
|
||||
if (!user) {
|
||||
// this.eventManager.write(null, 2, 0, { aboveLevel: 1 }, `User nicht gefunden`);
|
||||
levelId = 2;
|
||||
message = `Unbekannter User`
|
||||
return {token: null, levelId: levelId };
|
||||
// throw new Error(`User ${sAMAccountName} nicht gefunden`);
|
||||
}
|
||||
// if (user.password) throw new Error('Passwort bereits gesetzt');
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
user.password = hashedPassword;
|
||||
await user.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Login mit Speicherung des Tokens in der Datenbank
|
||||
*/
|
||||
async login(sAMAccountName, password) {
|
||||
const user = await this.Authentication.findOne({ where: { sAMAccountName } });
|
||||
|
||||
if (!user) {
|
||||
//this.eventManager.write(null, 2, null, null, `User ${sAMAccountName} nicht geufunden`)
|
||||
levelId = 2;
|
||||
message = `Unbekannter Benutzer`;
|
||||
return { token: null, levelId: levelId, message: message };
|
||||
// throw new Error('Unkown user');
|
||||
}
|
||||
if (!user.password) {
|
||||
this.setPassword(sAMAccountName, password);
|
||||
// this.eventManager.write(user.ObjectGUID, 2, null, null, 'User registration initialized')
|
||||
levelId = 1;
|
||||
message = `Benutzer nicht registiert`;
|
||||
return { token: null, levelId: levelId, message: message };
|
||||
// throw new Error('User not registered');
|
||||
}
|
||||
|
||||
const passwordMatch = await bcrypt.compare(password, user.password);
|
||||
if (!passwordMatch) {
|
||||
// this.eventManager.write(user.ObjectGUID, 2, null, null, 'Password doesn\'t match');
|
||||
levelId = 2;
|
||||
message = `Falsches Passwort`;
|
||||
return { token: null, levelId: levelId, message: message };
|
||||
// throw new Error('Wrong password');
|
||||
}
|
||||
|
||||
// Token erzeugen
|
||||
const payload = {
|
||||
sAMAccountName: user.sAMAccountName,
|
||||
mail: user.mail,
|
||||
givenName: user.givenName,
|
||||
sn: user.sn
|
||||
};
|
||||
|
||||
const token = jwt.sign(payload, this.SECRET_KEY, { expiresIn: '100y' });
|
||||
// Token in DB speichern
|
||||
user.refreshtoken = token;
|
||||
user.online = true;
|
||||
await user.save();
|
||||
|
||||
// this.eventManager.write(user.ObjectGUID, 1, null, null, 'Erfolgreich angemeldet');
|
||||
levelId = 0;
|
||||
message = `Erfolgreich angemeldet`;
|
||||
return { token: token, levelId: levelId, message: message };
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout löscht Token aus der DB
|
||||
*/
|
||||
async logout(sAMAccountName) {
|
||||
const user = await this.Authentication.findOne({ where: { sAMAccountName } });
|
||||
if (user) {
|
||||
user.refreshtoken = null;
|
||||
user.online = false;
|
||||
await user.save();
|
||||
levelId = 0;
|
||||
message = `Erfolgreich abgemeldet`;
|
||||
return { token: null, levelId: levelId, message: message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Token-Prüfung (über DB)
|
||||
*/
|
||||
async verifyUserToken(sAMAccountName) {
|
||||
const user = await this.Authentication.findOne({ where: { sAMAccountName } });
|
||||
if (!user || !user.refreshtoken) {
|
||||
levelId = 1,
|
||||
message = `Kein gültiger Token`;
|
||||
// throw new Error('Kein gespeicherter Token gefunden');
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = jwt.verify(user.refreshtoken, this.SECRET_KEY);
|
||||
levelId = 0;
|
||||
message = `User verifiziert`;
|
||||
return { valid: true, payload, user, levelId: levelId, message: message }
|
||||
} catch {
|
||||
levelId = 4;
|
||||
message = `Ungültiger Token`;
|
||||
return { valid: false, payload, user, levelId: levelId, message: message }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Express Middleware – prüft Token direkt aus DB anhand sAMAccountNamec
|
||||
*/
|
||||
authenticate() {
|
||||
return async (req, res, next) => {
|
||||
try {
|
||||
const sAMAccountName = req.cookies?.sAMAccountName;
|
||||
const objectGUID = req.cookies?.ObjectGUID;
|
||||
if (!sAMAccountName || !objectGUID) {
|
||||
return res.redirect('/login');
|
||||
// return res.status(401).json({ message: 'Kein Benutzer-Cookie gefunden' });
|
||||
}
|
||||
|
||||
const user = await this.Authentication.findOne({ where: { sAMAccountName } });
|
||||
if (!user || !user.refreshtoken) {
|
||||
return res.redirect('/login');
|
||||
// return res.status(401).json({ message: 'Benutzer oder Token nicht gefunden' });
|
||||
}
|
||||
|
||||
if (user.active === false) {
|
||||
return res.redirect('/login');
|
||||
// return res.status(401).json({ message: 'Benutzer ist nicht aktiv' });
|
||||
}
|
||||
|
||||
// Token aus DB prüfen
|
||||
const payload = jwt.verify(user.refreshtoken, this.SECRET_KEY);
|
||||
req.user = user;
|
||||
next();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return res.redirect('/login');
|
||||
// res.status(401).json({ message: 'Authentifizierung fehlgeschlagen' });
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AuthenticationManager;
|
||||
159
src/services/eventManager.js
Normal file
159
src/services/eventManager.js
Normal file
@@ -0,0 +1,159 @@
|
||||
const { Op } = require('sequelize');
|
||||
/**
|
||||
* Custom event logging class
|
||||
*
|
||||
*/
|
||||
class EventManager {
|
||||
/**
|
||||
* @param {*} eventlogModel - Use the eventlog database model for interact with the database
|
||||
* @param {*} socketManager - Get the administration socket for sending events to frontend
|
||||
*/
|
||||
constructor(app, eventlogModel, eventLogView, socketManager) {
|
||||
this.app = app;
|
||||
this.EventLog = eventlogModel;
|
||||
this.socketManager = socketManager;
|
||||
this.EventLogView = eventLogView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new log entry in EventLog database table
|
||||
* @param {string} [objectGuid] - MSSQL uniqueidentifier (UUID) or null = '00000000-0000-0000-0000-000000000000' AS 'SYSTEM'
|
||||
* @param {number} levelId - -1=test, 0=success, 1=log, 2=warn, 4=error, 8=throw_exception
|
||||
* @param {number} pluginId - ID of plugin
|
||||
* @param {Object} socketSending - Use socket to send error message to admin web frontends. Object: { active: true, levelId: 1 }
|
||||
* @param {Array} args - Message: string or comma seperated array of string
|
||||
* args[0].stack !== undefined - => Client side (error) stack and message
|
||||
*/
|
||||
async write(objectGuid, levelId, pluginName = null, ...args) {
|
||||
const err = new Error();
|
||||
const stackLine = args[0].stack !== undefined ? args[0].stack : err.stack.split('\n')[2]; // calls trace-line
|
||||
//const trace = stackLine.split("\n") !== undefined ? stackLine.split("\n")[1].trim() : stackLine.match(/\/.*\d+/)[0].replace(this.app.locals.path.root, '') || ''; // path:line:column
|
||||
const trace = stackLine?.split("\n")?.[1]?.trim() ?? stackLine?.match(/\/.*\d+/)?.[0]?.replace(this.app.locals.path.root, '') ?? '';
|
||||
|
||||
// const message = !Array.isArray(...args) ? [...args][0] : [...args][0][0]
|
||||
const message = args[0].stack !== undefined ? args[0].message : args.join('\r\n\t');
|
||||
|
||||
const convertLevel = levelId == -1 ? 'test' : levelId == 0 ? 'success' : levelId == 1 ? 'log' : levelId == 2 ? 'warn' : levelId == 4 ? 'error' : 'throw exception';
|
||||
const convertPluginName = pluginName == null ? 'SYSTEM' : pluginName;
|
||||
|
||||
const entry = await this.EventLog.create({
|
||||
Message: message,
|
||||
Trace: trace,
|
||||
Level_ID: levelId,
|
||||
PluginName: convertPluginName,
|
||||
ObjectGUID: (objectGuid == null || objectGuid === undefined ? '00000000-0000-0000-0000-000000000000' : objectGuid)
|
||||
});
|
||||
const newLogEntry = await this.EventLogView.findOne( { where: { ID: entry.ID }, plain: true } );
|
||||
if(levelId > -1) { // if not levelId = -1 | test-message
|
||||
this.socketManager.broadcast('admin', 'eventlog_table', newLogEntry);
|
||||
}
|
||||
|
||||
if(this.app.locals.configuration.debug.active && levelId >= this.app.locals.configuration.debug.levelId) {
|
||||
this.socketManager.broadcast('admin', 'eventlog', { levelId: levelId, pluginName: pluginName, datetime: `[${dateFormat(new Date(), 'yyyy-mm-dd HH:MM:SS')}]`, trace: `[${trace}]`, message: message })
|
||||
}
|
||||
|
||||
console.log(`[${dateFormat(new Date(), 'yyyy-mm-dd HH:MM:SS')}][${convertPluginName}][${convertLevel}][${trace}]:`, message.replaceAll('<br>', '\r\n'));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Only creates a new log entry in EventLog database table, without sending socketEvent
|
||||
* @param {string} [objectGuid] - MSSQL uniqueidentifier (UUID) or null = '00000000-0000-0000-0000-000000000000' AS 'SYSTEM'
|
||||
* @param {number} levelId - -1=test, 0=success, 1=log, 2=warn, 4=error, 8=throw_exception
|
||||
* @param {number} pluginId - ID of plugin
|
||||
* @param {Object} socketSending - Use socket to send error message to admin web frontends. Object: { active: true, levelId: 1 }
|
||||
* @param {Array} args - Message: string or comma seperated array of string
|
||||
* args[0].stack !== undefined - => Client side (error) stack and message
|
||||
*/
|
||||
async writeLog(objectGuid, levelId, pluginName = null, ...args) {
|
||||
const err = new Error();
|
||||
const stackLine = args[0].stack !== undefined ? args[0].stack : err.stack.split('\n')[2]; // calls trace-line
|
||||
const trace = args[0].stack !== undefined ? stackLine.split("\n")[1].trim() : stackLine.match(/\/.*\d+/)[0].replace(this.app.locals.path.root, ''); // path:line:column
|
||||
// const message = !Array.isArray(...args) ? [...args][0] : [...args][0][0]
|
||||
const message = args[0].stack !== undefined ? args[0].message : args.join('\r\n\t');
|
||||
|
||||
const convertLevel = levelId == -1 ? 'test' : levelId == 0 ? 'success' : levelId == 1 ? 'log' : levelId == 2 ? 'warn' : levelId == 4 ? 'error' : 'throw exception';
|
||||
const convertPluginName = pluginName == null ? 'SYSTEM' : pluginName;
|
||||
|
||||
const entry = await this.EventLog.create({
|
||||
Message: message,
|
||||
Trace: trace,
|
||||
Level_ID: levelId,
|
||||
PluginName: convertPluginName,
|
||||
ObjectGUID: (objectGuid == null || objectGuid === undefined ? '00000000-0000-0000-0000-000000000000' : objectGuid)
|
||||
});
|
||||
|
||||
const newLogEntry = await this.EventLogView.findOne( { where: { ID: entry.ID }, plain: true } );
|
||||
if(levelId > -1) { // if not levelId = -1 | test-message
|
||||
this.socketManager.broadcast('admin', 'eventlog_table', newLogEntry);
|
||||
}
|
||||
|
||||
console.log(`[${dateFormat(new Date(), 'yyyy-mm-dd HH:MM:SS')}][${convertPluginName}][${convertLevel}][${trace}]:`, message.replaceAll('<br>', '\r\n'));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clears the eventlog database table
|
||||
*/
|
||||
async clear() {
|
||||
const err = new Error();
|
||||
const stackLine = err.stack.split('\n')[2]; // calls trace-line
|
||||
const trace = stackLine.match(/\/.*\d+/)[0].replace(this.app.locals.path.root, ''); // path:line:column
|
||||
|
||||
const message = `${this.EventLog.tableName} cleared successfully`;
|
||||
await this.EventLog.destroy({
|
||||
where: {},
|
||||
truncate: true,
|
||||
});
|
||||
this.socketManager.broadcast('admin', 'eventlog', { levelId: 0, pluginName: 'SYSTEM', datetime: `[${dateFormat(new Date(), 'yyyy-mm-dd HH:MM:SS')}]`, trace: `[${trace}]`, message: message })
|
||||
console.log(`[${dateFormat(new Date(), 'yyyy-mm-dd HH:MM:SS')}][${trace}]`, message.replaceAll('<br>', '\r\n'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all eventlogs from database table
|
||||
*/
|
||||
async getAllEventLogs() {
|
||||
try {
|
||||
const logs = await this.EventLogView.findAll({ order: [['ID', 'DESC']] }); // Alle Zeilen abrufen
|
||||
const logsArray = logs.map(log => log.get({ plain: true })); // Sequelize-Objekte in reine JS-Objekte
|
||||
|
||||
return logsArray; // Rückgabe als Array
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen der EventLogs:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a specified range of eventlogs from database table
|
||||
*/
|
||||
async getEventLogs(fromID = null, toID) {
|
||||
try {
|
||||
const whereStatement = fromID != null ? {
|
||||
where: {
|
||||
ID: {
|
||||
[Op.gte]: fromID, // größer gleich vonID
|
||||
[Op.lte]: toID // kleiner gleich bisID
|
||||
}
|
||||
},
|
||||
order: [['ID', 'DESC']]
|
||||
} :
|
||||
{
|
||||
limit: toID,
|
||||
order: [['ID', 'DESC']]
|
||||
}
|
||||
|
||||
|
||||
const logs = await this.EventLogView.findAll(whereStatement);
|
||||
|
||||
const logsArray = logs.map(log => log.get({ plain: true }));
|
||||
return logsArray;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen der EventLogs:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = EventManager;
|
||||
187
src/services/fileSystemManager.js
Normal file
187
src/services/fileSystemManager.js
Normal file
@@ -0,0 +1,187 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
class FileSystemManager {
|
||||
/**
|
||||
* @param {string} jsonPath - Pfad zur JSON-Datei
|
||||
* @param {boolean} watch - ob die JSON-Datei überwacht werden soll (Live-Update)
|
||||
*/
|
||||
constructor() { }
|
||||
|
||||
loadAllFiles(path, fileextension = null) {
|
||||
let files = {};
|
||||
fs.readdirSync(path).forEach(file => {
|
||||
if (fileextension == null || file.endsWith(fileextension)) {
|
||||
const filefound = require(`${path}/${file}`);
|
||||
files = { ...files, ...filefound }; // zusammenführen
|
||||
}
|
||||
});
|
||||
return files;
|
||||
}
|
||||
|
||||
getAllFiles(path, fileextension = null) {
|
||||
let files = {};
|
||||
fs.readdirSync(path).forEach(file => {
|
||||
if (fileextension == null || file.endsWith(fileextension)) {
|
||||
const filefound = `${path}/${file}`;
|
||||
files = { ...files, ...filefound }; // zusammenführen
|
||||
}
|
||||
});
|
||||
return files;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Liest rekursiv Dateien und gibt nur die gewünschten Attribute zurück.
|
||||
*
|
||||
* @param {string} dirPath - Startverzeichnis
|
||||
* @param {string[]} attributes - gewünschte Attribute:
|
||||
* ['name','fullPath','size','lastModified','isDirectory','extension', ...]
|
||||
* @param {string|null} sortBy - Attribut zum Sortieren (z.B. 'lastModified' oder 'name')
|
||||
* @param {string} order - 'asc' oder 'desc'
|
||||
*/
|
||||
readFiles(dirPath, attributes = ['name', 'fullPath'], sortBy = null, order = 'asc') {
|
||||
let results = [];
|
||||
|
||||
const items = fs.readdirSync(dirPath);
|
||||
|
||||
for (const item of items) {
|
||||
const fullPath = path.join(dirPath, item);
|
||||
const stats = fs.statSync(fullPath);
|
||||
const isDir = stats.isDirectory();
|
||||
|
||||
// Objekt mit ALLEN möglichen Infos
|
||||
const allInfo = {
|
||||
name: item,
|
||||
nameWithoutExt: item.substring(0, item.indexOf('.')),
|
||||
fullPath: fullPath,
|
||||
size: stats.size,
|
||||
lastModified: stats.mtime,
|
||||
created: stats.birthtime,
|
||||
isDirectory: isDir,
|
||||
extension: isDir ? null : path.extname(item)
|
||||
};
|
||||
|
||||
if (isDir) {
|
||||
// rekursiv weitermachen
|
||||
results = results.concat(this.readFiles(fullPath, attributes, null, order));
|
||||
} else {
|
||||
// nur gewünschte Attribute ausgeben
|
||||
const filtered = {};
|
||||
for (const attr of attributes) {
|
||||
if (allInfo[attr] !== undefined) {
|
||||
filtered[attr] = allInfo[attr];
|
||||
}
|
||||
}
|
||||
results.push(filtered);
|
||||
}
|
||||
}
|
||||
|
||||
// Sortieren, falls gewünscht
|
||||
if (sortBy) {
|
||||
results.sort((a, b) => {
|
||||
if (a[sortBy] < b[sortBy]) return order === 'asc' ? -1 : 1;
|
||||
if (a[sortBy] > b[sortBy]) return order === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Sammelt verschiedene Pattern-Ergebnisse aus Dateien mehrerer Ordner.
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {string|string[]} options.folderPaths - Pfad oder Array von Pfaden zu Ordnern
|
||||
* @param {string} [options.extension='.js'] - Dateiendung
|
||||
* @param {Array<{ name: string, pattern: RegExp, mapFn?: Function }>} options.patterns - Liste von Pattern-Definitionen
|
||||
* @param {boolean} [options.recursive=true] - Unterordner durchsuchen?
|
||||
* @returns {Array} - kombinierte Ergebnisse aller Pattern
|
||||
*/
|
||||
collectFromFiles({
|
||||
folderPaths,
|
||||
extension = '.js',
|
||||
patterns,
|
||||
recursive = true
|
||||
}) {
|
||||
const results = [];
|
||||
|
||||
// if only one path selected
|
||||
const paths = Array.isArray(folderPaths) ? folderPaths : [folderPaths];
|
||||
|
||||
function readDir(dir) {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory() && recursive) {
|
||||
readDir(fullPath);
|
||||
} else if (entry.isFile() && entry.name.endsWith(extension)) {
|
||||
const content = fs.readFileSync(fullPath, 'utf8');
|
||||
|
||||
// 👉 NEU: fallback wenn keine patterns
|
||||
if (!patterns || patterns.length === 0) {
|
||||
results.push({
|
||||
file: entry.name,
|
||||
fullPath
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const { name, pattern, mapFn } of patterns) {
|
||||
let match;
|
||||
while ((match = pattern.exec(content)) !== null) {
|
||||
const mapped = mapFn
|
||||
? mapFn(match, entry.name, fullPath, name)
|
||||
: { file: entry.name, type: name, match: match[0] };
|
||||
results.push(mapped);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run through multiple paths
|
||||
for (const dir of paths) {
|
||||
if (fs.existsSync(dir)) {
|
||||
readDir(path.resolve(dir));
|
||||
} else {
|
||||
console.warn(`Ordner nicht gefunden: ${dir}`);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// JSON-Datei laden
|
||||
loadJSON(path) {
|
||||
try {
|
||||
const rawData = fs.readFileSync(path, 'utf8');
|
||||
return JSON.parse(rawData);
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Check file-/path
|
||||
exists(path) {
|
||||
try {
|
||||
const info = fs.statSync(path);
|
||||
if (!info.isFile() && !info.isDirectory()) return { status: false, levelId: 4, message: `${info.isFile() ? 'Datei' : 'Pfad'} ${path} existiert nicht` };
|
||||
return { status: true, levelId: 0, message: `${info.isFile() ? 'Datei' : 'Pfad'} existiert: ${path}` };
|
||||
} catch (err) {
|
||||
return ;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FileSystemManager;
|
||||
377
src/services/hotReload.js
Normal file
377
src/services/hotReload.js
Normal file
@@ -0,0 +1,377 @@
|
||||
// hotReloadJson.js
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { watch } = require('chokidar');
|
||||
|
||||
module.exports = {
|
||||
File: class {
|
||||
constructor(filePath, options = {}) {
|
||||
if (!filePath || typeof filePath !== 'string') {
|
||||
return { levelId: 4, pluginName: null, message: 'filePath muss ein gültiger String sein' };
|
||||
}
|
||||
|
||||
this.filePath = path.resolve(filePath);
|
||||
this.fileType = options.fileType || "json"; // "json" oder "js"
|
||||
this.historyLimit = options.historyLimit || 50;
|
||||
this.autoCreate = options.autoCreate || false;
|
||||
this.autoSaveJs = options.autoSaveJs || false; // neu: JS-Dateien optional speichern
|
||||
this.callbacks = [];
|
||||
this.history = [];
|
||||
this.redoStack = [];
|
||||
|
||||
if (!fs.existsSync(this.filePath)) {
|
||||
if(this.autoCreate) {
|
||||
fs.writeFileSync(this.filePath, this.fileType === 'json' ? '{ }' : 'module.exports = {}');
|
||||
} else {
|
||||
return { levelId: 4, pluginName: null, message: `Datei existiert nicht: ${this.filePath}` };
|
||||
}
|
||||
}
|
||||
|
||||
const initialData = this.loadFile();
|
||||
if (!initialData) {
|
||||
this.data = {};
|
||||
return { levelId: 4, pluginName: null, message: `Fehler beim Laden der Datei: ${this.filePath}` };
|
||||
} else {
|
||||
this.data = initialData;
|
||||
}
|
||||
|
||||
this.proxy = this.createProxy(this.data);
|
||||
this.pushHistory(null, this.deepCopy(this.data));
|
||||
|
||||
this.watcher = watch(this.filePath, { ignoreInitial: true });
|
||||
this.watcher.on('change', () => this.reload());
|
||||
}
|
||||
|
||||
loadJS() {
|
||||
try {
|
||||
delete require.cache[require.resolve(this.filePath)];
|
||||
const module = require(this.filePath);
|
||||
if (typeof module !== "object") {
|
||||
console.warn("[LiveJSON] JS-Datei exportiert kein Objekt:", this.filePath);
|
||||
return {};
|
||||
}
|
||||
return this.deepCopy(module);
|
||||
} catch (err) {
|
||||
console.error(`[LiveJSON] Fehler beim Laden der JS-Datei ${this.filePath}:`, err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
loadJSON() {
|
||||
try {
|
||||
const raw = fs.readFileSync(this.filePath, 'utf-8');
|
||||
return JSON.parse(raw);
|
||||
} catch (err) {
|
||||
console.error(`[LiveJSON] Fehler beim Laden von ${this.filePath}:`, err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
loadFile() {
|
||||
if (this.fileType === "js") return this.loadJS();
|
||||
return this.loadJSON();
|
||||
}
|
||||
|
||||
saveToFile() {
|
||||
try {
|
||||
if (this.fileType === 'json') {
|
||||
fs.writeFileSync(this.filePath, JSON.stringify(this.data, null, 2), 'utf-8');
|
||||
} else if (this.fileType === 'js' && this.autoSaveJs) {
|
||||
const content = 'module.exports = ' + JSON.stringify(this.data, null, 2);
|
||||
fs.writeFileSync(this.filePath, content, 'utf-8');
|
||||
}
|
||||
return { levelId: 0, pluginName: null, message: 'Änderungen erfolgreich gespeichert' };
|
||||
} catch (err) {
|
||||
return { levelId: 4, pluginName: null, message: `Fehler beim Schreiben in Datei: ${err.message}` };
|
||||
}
|
||||
}
|
||||
|
||||
createProxy(obj) {
|
||||
const self = this;
|
||||
return new Proxy(obj, {
|
||||
get(target, prop) {
|
||||
const value = target[prop];
|
||||
if (value && typeof value === 'object') {
|
||||
return self.createProxy(value);
|
||||
}
|
||||
return value;
|
||||
},
|
||||
set(target, prop, value) {
|
||||
const oldValue = self.deepCopy(target);
|
||||
const result = Reflect.set(target, prop, value);
|
||||
const newValue = self.deepCopy(target);
|
||||
self.pushHistory(oldValue, newValue);
|
||||
self.saveToFile();
|
||||
self.triggerCallbacks(oldValue, newValue);
|
||||
return result;
|
||||
},
|
||||
deleteProperty(target, prop) {
|
||||
const oldValue = self.deepCopy(target);
|
||||
const result = Reflect.deleteProperty(target, prop);
|
||||
const newValue = self.deepCopy(target);
|
||||
self.pushHistory(oldValue, newValue);
|
||||
self.saveToFile();
|
||||
self.triggerCallbacks(oldValue, newValue);
|
||||
return result;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
reload() {
|
||||
setTimeout(() => {
|
||||
const oldData = this.deepCopy(this.data);
|
||||
const newData = this.loadFile();
|
||||
if (newData) {
|
||||
this.mergeObjects(this.data, newData);
|
||||
this.pushHistory(oldData, this.deepCopy(this.data));
|
||||
// Trigger Callback jetzt mit absPath
|
||||
this.triggerCallbacks(oldData, this.data, this.filePath);
|
||||
}
|
||||
}, 100);
|
||||
return { levelId: 0, message: 'Reload initiiert' };
|
||||
}
|
||||
|
||||
deepCopy(obj) {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
pushHistory(oldState, newState) {
|
||||
const entry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
oldState,
|
||||
newState
|
||||
};
|
||||
this.history.push(entry);
|
||||
if (this.history.length > this.historyLimit) this.history.shift();
|
||||
this.redoStack = [];
|
||||
}
|
||||
|
||||
undo() {
|
||||
if (this.history.length < 2) return { levelId: 4, message: 'Nichts zum Rückgängigmachen' };
|
||||
const last = this.history.pop();
|
||||
this.redoStack.push(last);
|
||||
const prev = this.history[this.history.length - 1];
|
||||
this.mergeObjects(this.data, prev.newState, true);
|
||||
this.saveToFile();
|
||||
this.triggerCallbacks(last.newState, prev.newState);
|
||||
return { levelId: 0, message: 'Undo erfolgreich', state: prev.newState };
|
||||
}
|
||||
|
||||
redo() {
|
||||
if (this.redoStack.length === 0) return { levelId: 4, message: 'Nichts zum Wiederherstellen' };
|
||||
const next = this.redoStack.pop();
|
||||
this.history.push(next);
|
||||
this.mergeObjects(this.data, next.newState, true);
|
||||
this.saveToFile();
|
||||
this.triggerCallbacks(next.oldState, next.newState);
|
||||
return { levelId: 0, message: 'Redo erfolgreich', state: next.newState };
|
||||
}
|
||||
|
||||
// Rekursive Diff inkl. Arrays
|
||||
diff(oldObj, newObj) {
|
||||
if (Array.isArray(oldObj) && Array.isArray(newObj)) return this.diffArray(oldObj, newObj);
|
||||
if (this.isObject(oldObj) && this.isObject(newObj)) {
|
||||
const changes = {};
|
||||
const allKeys = new Set([...Object.keys(oldObj || {}), ...Object.keys(newObj || {})]);
|
||||
allKeys.forEach(key => {
|
||||
const oldVal = oldObj[key];
|
||||
const newVal = newObj[key];
|
||||
if (this.isObject(oldVal) && this.isObject(newVal)) {
|
||||
const childDiff = this.diff(oldVal, newVal);
|
||||
if (Object.keys(childDiff).length > 0) changes[key] = childDiff;
|
||||
} else if (Array.isArray(oldVal) && Array.isArray(newVal)) {
|
||||
const arrayDiff = this.diffArray(oldVal, newVal);
|
||||
if (arrayDiff.length > 0) changes[key] = arrayDiff;
|
||||
} else if (oldVal !== newVal) {
|
||||
changes[key] = { old: oldVal, new: newVal };
|
||||
}
|
||||
});
|
||||
return changes;
|
||||
}
|
||||
return oldObj !== newObj ? { old: oldObj, new: newObj } : {};
|
||||
}
|
||||
|
||||
// Diff für Arrays auf Elementebene
|
||||
diffArray(oldArr, newArr) {
|
||||
const maxLen = Math.max(oldArr.length, newArr.length);
|
||||
const changes = [];
|
||||
for (let i = 0; i < maxLen; i++) {
|
||||
if (i >= oldArr.length) {
|
||||
changes.push({ index: i, old: undefined, new: newArr[i] });
|
||||
} else if (i >= newArr.length) {
|
||||
changes.push({ index: i, old: oldArr[i], new: undefined });
|
||||
} else if (JSON.stringify(oldArr[i]) !== JSON.stringify(newArr[i])) {
|
||||
changes.push({ index: i, old: oldArr[i], new: newArr[i] });
|
||||
}
|
||||
}
|
||||
return changes;
|
||||
}
|
||||
|
||||
isObject(value) {
|
||||
return value && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
|
||||
// Merge für Objekte und Arrays
|
||||
mergeObjects(target, source, removeExtra = false) {
|
||||
if (Array.isArray(target) && Array.isArray(source)) {
|
||||
target.length = 0;
|
||||
source.forEach((el) => target.push(this.deepCopy(el)));
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(target).forEach(key => {
|
||||
if (source[key] === undefined && removeExtra) delete target[key];
|
||||
});
|
||||
|
||||
Object.keys(source).forEach(key => {
|
||||
if (this.isObject(source[key])) {
|
||||
if (!target[key] || !this.isObject(target[key])) target[key] = {};
|
||||
this.mergeObjects(target[key], source[key], removeExtra);
|
||||
} else if (Array.isArray(source[key])) {
|
||||
target[key] = [];
|
||||
this.mergeObjects(target[key], source[key], removeExtra);
|
||||
} else {
|
||||
target[key] = source[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onChange(cb) {
|
||||
if (typeof cb === 'function') this.callbacks.push(cb);
|
||||
}
|
||||
|
||||
triggerCallbacks(oldState, newState, absPath = null) {
|
||||
const delta = this.diff(oldState, newState);
|
||||
this.callbacks.forEach(cb => cb({ oldState, newState, delta, path: absPath }));
|
||||
}
|
||||
|
||||
get live() {
|
||||
return this.proxy;
|
||||
}
|
||||
|
||||
close() {
|
||||
this.watcher.close();
|
||||
return { levelId: 0, pluginName: null, message: 'Watcher gestoppt' };
|
||||
}
|
||||
},
|
||||
|
||||
Folder: class {
|
||||
constructor(folderPath, options = {}) {
|
||||
if (!folderPath || typeof folderPath !== 'string') {
|
||||
return {
|
||||
levelId: 4,
|
||||
pluginName: null,
|
||||
message: "folderPath muss ein gültiger String sein"
|
||||
};
|
||||
}
|
||||
|
||||
this.folderPath = path.resolve(folderPath);
|
||||
this.files = {}; // relPath -> File-Instanz
|
||||
this.liveProxies = {}; // relPath -> Proxy für live Zugriff
|
||||
|
||||
this.options = {
|
||||
ignoreInitial: false,
|
||||
persistent: true,
|
||||
ignored: options.ignored ?? null,
|
||||
};
|
||||
|
||||
// Callbacks
|
||||
this.handlers = {
|
||||
onAdd: options.onAdd || (() => {}),
|
||||
onChange: options.onChange || (() => {}),
|
||||
onUnlink: options.onUnlink || (() => {}),
|
||||
onAddDir: options.onAddDir || (() => {}),
|
||||
onUnlinkDir: options.onUnlinkDir || (() => {}),
|
||||
onReady: options.onReady || (() => {}),
|
||||
onError: options.onError || (() => {}),
|
||||
};
|
||||
|
||||
try {
|
||||
this.watcher = watch(this.folderPath, this.options);
|
||||
this.bindEvents();
|
||||
return { levelId: 0, pluginName: null, message: "SmartFolderWatcher mit History erfolgreich initialisiert" };
|
||||
} catch (err) {
|
||||
return { levelId: 4, pluginName: null, message: `Watcher konnte nicht gestartet werden: ${err.message}` };
|
||||
}
|
||||
}
|
||||
|
||||
// Datei initial laden / als File-Instanz tracken
|
||||
loadTrackedFile(absPath) {
|
||||
const rel = path.relative(this.folderPath, absPath);
|
||||
const ext = path.extname(absPath).toLowerCase();
|
||||
const fileType = ext === '.js' ? 'js' : 'json';
|
||||
|
||||
// File-Instanz erstellen, falls nicht existiert
|
||||
if (!this.files[rel]) {
|
||||
const { File } = module.exports;
|
||||
const fileInstance = new File(absPath, { fileType, autoCreate: true, autoSaveJs: true });
|
||||
this.files[rel] = fileInstance;
|
||||
this.liveProxies[rel] = fileInstance.live;
|
||||
} else {
|
||||
// reload falls schon bekannt
|
||||
this.files[rel].reload();
|
||||
}
|
||||
|
||||
return this.files[rel];
|
||||
}
|
||||
|
||||
// Chokidar Events
|
||||
bindEvents() {
|
||||
this.watcher
|
||||
.on("add", (file) => {
|
||||
const rel = path.relative(this.folderPath, file);
|
||||
const fileInstance = this.loadTrackedFile(file);
|
||||
this.handlers.onAdd(rel, fileInstance.data, file); // rel, Inhalt, absPath
|
||||
})
|
||||
.on("change", (file) => {
|
||||
const rel = path.relative(this.folderPath, file);
|
||||
const fileInstance = this.loadTrackedFile(file);
|
||||
this.handlers.onChange(rel, fileInstance.data, file);
|
||||
})
|
||||
.on("unlink", (file) => {
|
||||
const rel = path.relative(this.folderPath, file);
|
||||
delete this.liveProxies[rel];
|
||||
delete this.files[rel];
|
||||
this.handlers.onUnlink(rel, file);
|
||||
})
|
||||
.on("addDir", (dir) => {
|
||||
const rel = path.relative(this.folderPath, dir);
|
||||
this.handlers.onAddDir(rel, dir);
|
||||
})
|
||||
.on("unlinkDir", (dir) => {
|
||||
const rel = path.relative(this.folderPath, dir);
|
||||
this.handlers.onUnlinkDir(rel, dir);
|
||||
})
|
||||
.on("ready", () => {
|
||||
this.handlers.onReady(this.liveProxies);
|
||||
})
|
||||
.on("error", (err) => {
|
||||
this.handlers.onError(err);
|
||||
});
|
||||
}
|
||||
|
||||
// Zugriff auf alle live Proxies
|
||||
get live() {
|
||||
return this.liveProxies;
|
||||
}
|
||||
|
||||
// Undo / Redo auf Dateiebene
|
||||
undoFile(relPath) {
|
||||
if (!this.files[relPath]) return { levelId: 4, message: 'Datei nicht gefunden' };
|
||||
return this.files[relPath].undo();
|
||||
}
|
||||
|
||||
redoFile(relPath) {
|
||||
if (!this.files[relPath]) return { levelId: 4, message: 'Datei nicht gefunden' };
|
||||
return this.files[relPath].redo();
|
||||
}
|
||||
|
||||
close() {
|
||||
this.watcher.close();
|
||||
// close alle File-Watcher
|
||||
Object.values(this.files).forEach(f => f.close());
|
||||
return { levelId: 0, pluginName: null, message: "FolderWatcher gestoppt" };
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
302
src/services/identityManager.js
Normal file
302
src/services/identityManager.js
Normal file
@@ -0,0 +1,302 @@
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
class IdentityManager {
|
||||
constructor(adManager, AuthenticationModel) {
|
||||
this.ad = adManager || null;
|
||||
this.Authentication = AuthenticationModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* -----------------------------------------------------
|
||||
* REQUIRED FIELDS (MANUAL USER)
|
||||
* -----------------------------------------------------
|
||||
*/
|
||||
REQUIRED_FIELDS = [
|
||||
'sAMAccountName',
|
||||
'mail',
|
||||
'givenName',
|
||||
'sn',
|
||||
'password'
|
||||
];
|
||||
|
||||
/**
|
||||
* -----------------------------------------------------
|
||||
* VALIDATE MANUAL USER
|
||||
* -----------------------------------------------------
|
||||
*/
|
||||
validateManualUser(user) {
|
||||
const missing = [];
|
||||
|
||||
for (const field of this.REQUIRED_FIELDS) {
|
||||
if (
|
||||
user[field] === undefined ||
|
||||
user[field] === null ||
|
||||
user[field] === ''
|
||||
) {
|
||||
missing.push(field);
|
||||
}
|
||||
}
|
||||
|
||||
if (missing.length) {
|
||||
throw new Error(
|
||||
`Fehlende Pflichtfelder: ${missing.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
// 🔍 Optional: einfache Zusatzvalidierungen
|
||||
if (user.mail && !user.mail.includes('@')) {
|
||||
throw new Error('Ungültige E-Mail-Adresse');
|
||||
}
|
||||
|
||||
if (user.password && user.password.length < 6) {
|
||||
throw new Error('Passwort muss mindestens 6 Zeichen lang sein');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* -----------------------------------------------------
|
||||
* FIXED MAPPING (AD → Authentication)
|
||||
* -----------------------------------------------------
|
||||
*/
|
||||
mapAdObject(obj) {
|
||||
if (!obj || !obj.objectGUID) return null;
|
||||
|
||||
return {
|
||||
ObjectGUID: obj.objectGUID,
|
||||
sAMAccountName: obj.sAMAccountName || obj.cn || null,
|
||||
mail: obj.mail || null,
|
||||
givenName: obj.givenName || null,
|
||||
sn: obj.sn || null,
|
||||
employeeID: obj.employeeID || null,
|
||||
title: obj.title || null,
|
||||
department: obj.department || null,
|
||||
streetAddress: obj.streetAddress || null,
|
||||
userAccountControl_ID: obj.userAccountControl || null,
|
||||
authenticationType_ID: 1,
|
||||
telephoneNumber: obj.telephoneNumber || null,
|
||||
physicalDeliveryOfficeName: obj.physicalDeliveryOfficeName || null,
|
||||
distinguishedName: obj.dn || null,
|
||||
password: null,
|
||||
refreshtoken: null,
|
||||
active: true,
|
||||
online: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* -----------------------------------------------------
|
||||
* DEDUP (wie SQL UNION)
|
||||
* -----------------------------------------------------
|
||||
*/
|
||||
deduplicateByGUID(items) {
|
||||
const map = new Map();
|
||||
|
||||
for (const item of items) {
|
||||
if (!item?.ObjectGUID) continue;
|
||||
map.set(item.ObjectGUID, item);
|
||||
}
|
||||
|
||||
return Array.from(map.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* -----------------------------------------------------
|
||||
* TABLE CHECK / CREATE
|
||||
* -----------------------------------------------------
|
||||
*/
|
||||
async ensureTable() {
|
||||
const qi = this.Authentication.sequelize.getQueryInterface();
|
||||
const tables = await qi.showAllTables();
|
||||
|
||||
const exists = tables.includes('Authentication');
|
||||
|
||||
if (!exists) {
|
||||
await this.Authentication.sync();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* -----------------------------------------------------
|
||||
* CORE SYNC (INTELLIGENT)
|
||||
* -----------------------------------------------------
|
||||
*/
|
||||
async syncFromAD() {
|
||||
if (!this.ad) {
|
||||
throw new Error('AD nicht konfiguriert');
|
||||
}
|
||||
|
||||
const [users, groups, computers] = await Promise.all([
|
||||
this.ad.findUsers('*'),
|
||||
this.ad.findGroups('*'),
|
||||
this.ad.getComputers()
|
||||
]);
|
||||
|
||||
const mapped = this.deduplicateByGUID([
|
||||
...users.map(u => this.mapAdObject(u)),
|
||||
...groups.map(g => this.mapAdObject(g)),
|
||||
...computers.map(c => this.mapAdObject(c))
|
||||
].filter(Boolean));
|
||||
|
||||
if (!mapped.length) {
|
||||
return { total: 0, deactivated: 0 };
|
||||
}
|
||||
|
||||
await this.Authentication.bulkCreate(mapped, {
|
||||
updateOnDuplicate: [
|
||||
'mail',
|
||||
'givenName',
|
||||
'sn',
|
||||
'employeeID',
|
||||
'title',
|
||||
'department',
|
||||
'streetAddress',
|
||||
'userAccountControl_ID',
|
||||
'telephoneNumber',
|
||||
'physicalDeliveryOfficeName',
|
||||
'distinguishedName',
|
||||
'active'
|
||||
]
|
||||
});
|
||||
|
||||
const existing = await this.Authentication.findAll({
|
||||
where: { authenticationType_ID: 1 },
|
||||
attributes: ['ObjectGUID']
|
||||
});
|
||||
|
||||
const adGuids = new Set(mapped.map(u => u.ObjectGUID));
|
||||
|
||||
const toDeactivate = existing
|
||||
.filter(e => !adGuids.has(e.ObjectGUID))
|
||||
.map(e => e.ObjectGUID);
|
||||
|
||||
if (toDeactivate.length) {
|
||||
await this.Authentication.update(
|
||||
{ active: false },
|
||||
{
|
||||
where: {
|
||||
ObjectGUID: toDeactivate
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
total: mapped.length,
|
||||
deactivated: toDeactivate.length,
|
||||
adGuids: Array.from(adGuids)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* -----------------------------------------------------
|
||||
* OPTIONAL: HARD DELETE
|
||||
* -----------------------------------------------------
|
||||
*/
|
||||
async removeDeletedADObjects(adGuids) {
|
||||
return this.Authentication.destroy({
|
||||
where: {
|
||||
authenticationType_ID: 1,
|
||||
ObjectGUID: {
|
||||
[Op.notIn]: adGuids
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* -----------------------------------------------------
|
||||
* RECREATE
|
||||
* -----------------------------------------------------
|
||||
*/
|
||||
async recreateAuthentications(hardReset = false) {
|
||||
let message = '';
|
||||
|
||||
const exists = await this.ensureTable();
|
||||
|
||||
if (!exists) {
|
||||
message = 'Tabelle wurde neu erstellt ';
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.syncFromAD();
|
||||
|
||||
message += `Sync abgeschlossen (${result.total} Objekte)`;
|
||||
|
||||
if (result.deactivated) {
|
||||
message += `, ${result.deactivated} deaktiviert`;
|
||||
}
|
||||
|
||||
if (hardReset) {
|
||||
const deleted = await this.removeDeletedADObjects(result.adGuids);
|
||||
message += `, ${deleted} gelöscht`;
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
message += 'Fehler: ' + err.message;
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* -----------------------------------------------------
|
||||
* MANUAL USER (MIT VALIDATION)
|
||||
* -----------------------------------------------------
|
||||
*/
|
||||
async createManualUser(user) {
|
||||
this.validateManualUser(user);
|
||||
|
||||
return this.Authentication.create({
|
||||
...user,
|
||||
authenticationType_ID: 2,
|
||||
active: true,
|
||||
online: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* -----------------------------------------------------
|
||||
* MANUAL BULK (MIT VALIDATION)
|
||||
* -----------------------------------------------------
|
||||
*/
|
||||
async createManualUsers(users) {
|
||||
const errors = [];
|
||||
|
||||
users.forEach((user, index) => {
|
||||
try {
|
||||
this.validateManualUser(user);
|
||||
} catch (err) {
|
||||
errors.push(`User ${index}: ${err.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (errors.length) {
|
||||
throw new Error(errors.join(' | '));
|
||||
}
|
||||
|
||||
return this.Authentication.bulkCreate(
|
||||
users.map(user => ({
|
||||
...user,
|
||||
authenticationType_ID: 2,
|
||||
active: true,
|
||||
online: false
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* -----------------------------------------------------
|
||||
* GET USER
|
||||
* -----------------------------------------------------
|
||||
*/
|
||||
async getUser(username) {
|
||||
return this.Authentication.findOne({
|
||||
where: { sAMAccountName: username }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = IdentityManager;
|
||||
137
src/services/notifyTrayManager.js
Normal file
137
src/services/notifyTrayManager.js
Normal file
@@ -0,0 +1,137 @@
|
||||
class notifyTrayManager {
|
||||
/**
|
||||
* @param {object} models - { UserNotificationObjects, UserNotifications }
|
||||
*/
|
||||
constructor(notifyTrayModel, notifyTrayView, notifyTrayObject) {
|
||||
this.objects = notifyTrayObject;
|
||||
this.view = notifyTrayView;
|
||||
this.notifications = notifyTrayModel;
|
||||
}
|
||||
|
||||
//--------------------------------------------
|
||||
// 1. Notification Object erstellen oder updaten
|
||||
//--------------------------------------------
|
||||
async upsertObject({ id = null, pluginName, message, json, actionRequired = false, expiresAt = null }) {
|
||||
if (id) {
|
||||
// ID existiert → upsert
|
||||
return this.objects.upsert({
|
||||
ID: id,
|
||||
PluginName: pluginName,
|
||||
Message: message,
|
||||
JSON: json ? JSON.stringify(json) : null,
|
||||
ActionRequired: actionRequired,
|
||||
CreatedAt: new Date(),
|
||||
ExpiresAt: expiresAt
|
||||
});
|
||||
} else {
|
||||
// ID null → neue Zeile einfügen, Auto-Increment nutzen
|
||||
const obj = await this.objects.create({
|
||||
PluginName: pluginName,
|
||||
Message: message,
|
||||
JSON: json ? JSON.stringify(json) : null,
|
||||
ActionRequired: actionRequired,
|
||||
CreatedAt: new Date(),
|
||||
ExpiresAt: expiresAt
|
||||
});
|
||||
|
||||
// zurückgeben inklusive der generierten ID
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------
|
||||
// 2. User benachrichtigen (insert)
|
||||
//--------------------------------------------
|
||||
async notifyUsers({ objectId, objectGuids = [] }) {
|
||||
|
||||
if (!objectGuids.length) return;
|
||||
const notificationsToCreate = objectGuids.map(guid => ({
|
||||
ObjectGUID: guid,
|
||||
NotifyTrayObject_ID: objectId,
|
||||
}));
|
||||
return this.notifications.bulkCreate(notificationsToCreate);
|
||||
}
|
||||
|
||||
//--------------------------------------------
|
||||
// 3. Offene Notifications abfragen
|
||||
//--------------------------------------------
|
||||
async getOpenNotifications(objectGuid) {
|
||||
return await this.view.findAll({
|
||||
where: {
|
||||
ObjectGUID: objectGuid,
|
||||
SeenAt: null
|
||||
},
|
||||
// include: [{
|
||||
// model: this.objects,
|
||||
// as: 'NotificationObject',
|
||||
// required: true, // join zwingend
|
||||
// where: {
|
||||
// [this.objects.sequelize.Op.Or]: [
|
||||
// { ExpiresAt: null },
|
||||
// { ExpiresAt: { [this.objects.sequelize.Op.gt]: new Date() } }
|
||||
// ]
|
||||
// }
|
||||
// }],
|
||||
order: [[ 'SeenAt', 'ASC']]
|
||||
});
|
||||
}
|
||||
|
||||
//--------------------------------------------
|
||||
// 4. Einzelne Notification als gesehen markieren
|
||||
//--------------------------------------------
|
||||
async markAsSeen(objectGuid, notificationId, value) {
|
||||
return this.notifications.update(
|
||||
{ SeenAt: value ? new Date() : null },
|
||||
{ where: { ID: notificationId, ObjectGUID: objectGuid } }
|
||||
);
|
||||
}
|
||||
|
||||
//--------------------------------------------
|
||||
// 5. Alle Notifications eines Users als gesehen markieren
|
||||
//--------------------------------------------
|
||||
async markAllAsSeen(objectGuid, value) {
|
||||
return this.notifications.update(
|
||||
{ SeenAt: value ? new Date() : null },
|
||||
{ where: { ObjectGUID: objectGuid, SeenAt: null } }
|
||||
);
|
||||
}
|
||||
|
||||
//--------------------------------------------
|
||||
// 6. Komplett-Flow: Object upserten + User benachrichtigen
|
||||
//--------------------------------------------
|
||||
async createAndNotify({ objectId = null, pluginName, json, actionRequired, message, objectGuids, expiresAt }) {
|
||||
if(objectGuids === null || !objectGuids.length) {
|
||||
throw 'There is no user to notify for tray';
|
||||
}
|
||||
let obj;
|
||||
if (objectId) {
|
||||
obj = await this.upsertObject({
|
||||
id: objectId,
|
||||
pluginName,
|
||||
message,
|
||||
json,
|
||||
actionRequired,
|
||||
expiresAt: Date.now()
|
||||
});
|
||||
} else {
|
||||
obj = await this.upsertObject({
|
||||
id: null,
|
||||
pluginName,
|
||||
message,
|
||||
json,
|
||||
actionRequired,
|
||||
expiresAt: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-Increment ID korrekt für Notifications nutzen
|
||||
const finalObjectId = obj.ID || obj.id; // Sequelize gibt je nach DB dialect mal 'ID' oder 'id'
|
||||
|
||||
return await this.notifyUsers({
|
||||
objectId: finalObjectId,
|
||||
objectGuids
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = notifyTrayManager;
|
||||
497
src/services/pluginManager.js
Normal file
497
src/services/pluginManager.js
Normal file
@@ -0,0 +1,497 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const fse = require('fs-extra');
|
||||
const expressHandlebars = require('express-handlebars');
|
||||
const { execSync } = require('child_process');
|
||||
const { File: HotReload } = require(`@services/hotReload.js`);
|
||||
|
||||
class PluginManager {
|
||||
constructor(app, pluginModel, pluginBasePath, filePermissions, service) {
|
||||
this.app = app;
|
||||
this.Plugin = pluginModel;
|
||||
this.pluginBasePath = pluginBasePath;
|
||||
this.filePermissions = filePermissions;
|
||||
this.service = service;
|
||||
|
||||
this.plugins = new Map();
|
||||
|
||||
// FIX: metadata jetzt Map (aber Struktur bleibt gleich)
|
||||
this.metadata = new Map();
|
||||
|
||||
this.hbsInstance = expressHandlebars.create({
|
||||
extname: '.hbs',
|
||||
defaultLayout: 'main',
|
||||
});
|
||||
|
||||
if(!fs.existsSync(this.pluginBasePath)) {
|
||||
fs.mkdirSync(this.pluginBasePath)
|
||||
}
|
||||
}
|
||||
|
||||
async loadAll() {
|
||||
if (!fs.existsSync(this.pluginBasePath)) {
|
||||
return [{ levelId: 4, message: `Plugin-Pfad existiert nicht: ${this.pluginBasePath}` }];
|
||||
}
|
||||
|
||||
const pluginDirs = fs.readdirSync(this.pluginBasePath)
|
||||
.filter(f => fs.lstatSync(path.join(this.pluginBasePath, f)).isDirectory());
|
||||
|
||||
const loadPromises = pluginDirs.map(dir => this.load(dir));
|
||||
return await Promise.all(loadPromises);
|
||||
}
|
||||
|
||||
async load(name, withActivate = false) {
|
||||
const pluginPath = path.join(this.pluginBasePath, name);
|
||||
const metadataPath = path.join(pluginPath, 'plugin.json');
|
||||
|
||||
const Plugin = require(pluginPath);
|
||||
const instance = new Plugin(pluginPath, this.app, this.service);
|
||||
|
||||
let result;
|
||||
|
||||
try {
|
||||
// FIX: HotReload pro Plugin korrekt speichern
|
||||
const meta = new HotReload(metadataPath, {
|
||||
historyLimit: 50,
|
||||
autoCreate: true
|
||||
});
|
||||
|
||||
this.metadata.set(name, meta);
|
||||
|
||||
if (Object.keys(meta.live).length === 0) {
|
||||
meta.live = this.__pluginTemplate(name);
|
||||
meta.saveToFile();
|
||||
this.__setPermissions(pluginPath);
|
||||
}
|
||||
|
||||
// Routes
|
||||
const routesPath = path.join(pluginPath, 'routes.js');
|
||||
if (fs.existsSync(routesPath)) {
|
||||
const routes = require(routesPath);
|
||||
routes(this.app, pluginPath, meta.live, this.service);
|
||||
}
|
||||
|
||||
// Sockets
|
||||
const socketsPath = path.join(pluginPath, 'sockets.js');
|
||||
if (fs.existsSync(socketsPath)) {
|
||||
const sockets = require(socketsPath);
|
||||
sockets(this.app, pluginPath, meta.live, this.service);
|
||||
}
|
||||
|
||||
this.app.use(`/${name}`, require('express').static(path.join(pluginPath, 'public')));
|
||||
|
||||
const viewsDir = path.join(pluginPath, 'views');
|
||||
|
||||
this.app.set("views", [
|
||||
...(Array.isArray(this.app.get("views")) ? this.app.get("views") : [this.app.get("views")]),
|
||||
path.join(this.pluginBasePath)
|
||||
]);
|
||||
|
||||
this.registerPartialsRecursive(viewsDir, `plugins/${name}`);
|
||||
|
||||
await this.Plugin.upsert({
|
||||
Name: name,
|
||||
Active: withActivate ? true : (await this.Plugin.findOne({ where: { Name: meta.live.name }})).Active,
|
||||
Version: meta.live.version
|
||||
});
|
||||
|
||||
const static_meta = this.service.get('fileSystemManager').loadJSON(metadataPath);
|
||||
|
||||
this.plugins.set(name, {
|
||||
...static_meta,
|
||||
pluginPath,
|
||||
viewPath: viewsDir,
|
||||
metadataPath
|
||||
});
|
||||
|
||||
meta.live.active = withActivate ? true : (await this.Plugin.findOne({ where: { Name: meta.live.name }})).Active;
|
||||
|
||||
result = {
|
||||
status: 'load',
|
||||
pluginName: name,
|
||||
metadata: meta.live,
|
||||
levelId: 0,
|
||||
message: `Plugin ${name} geladen`
|
||||
};
|
||||
|
||||
return result;
|
||||
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
return {
|
||||
status: 'load',
|
||||
pluginName: name,
|
||||
levelId: 4,
|
||||
message: [err.message, err]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async unload(name) {
|
||||
let result;
|
||||
const pluginPath = path.join(this.pluginBasePath, name);
|
||||
const metadataPath = path.join(pluginPath, 'plugin.json');
|
||||
|
||||
try {
|
||||
if (!this.plugins.has(name)) {
|
||||
return {
|
||||
status: 'unload',
|
||||
pluginName: name,
|
||||
levelId: 4,
|
||||
message: 'Plugin nicht vorhanden'
|
||||
};
|
||||
}
|
||||
|
||||
// FIX: korrektes metadata handling
|
||||
const meta = this.metadata.get(name);
|
||||
|
||||
if (meta) {
|
||||
meta.live.active = false;
|
||||
meta.saveToFile();
|
||||
}
|
||||
|
||||
await this.Plugin.update({ Active: false }, { where: { Name: name } });
|
||||
|
||||
result = {
|
||||
status: 'unload',
|
||||
pluginName: name,
|
||||
levelId: 0,
|
||||
message: `Plugin ${name} entladen`
|
||||
};
|
||||
|
||||
return result;
|
||||
|
||||
} catch (err) {
|
||||
return {
|
||||
status: 'unload',
|
||||
pluginName: name,
|
||||
levelId: 4,
|
||||
message: [err.message, err]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async reload(name) {
|
||||
const unloadResult = await this.unload(name);
|
||||
if (unloadResult.levelId === 4) return unloadResult;
|
||||
|
||||
delete require.cache[require.resolve(path.join(this.pluginBasePath, name))];
|
||||
|
||||
return await this.load(name);
|
||||
}
|
||||
|
||||
async delete(name) {
|
||||
const pluginPath = path.join(this.pluginBasePath, name);
|
||||
|
||||
try {
|
||||
await this.unload(name);
|
||||
|
||||
this.plugins.delete(name);
|
||||
await this.Plugin.destroy({ where: { Name: name } });
|
||||
|
||||
if (fs.existsSync(pluginPath)) {
|
||||
await fse.remove(pluginPath);
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'delete',
|
||||
pluginName: name,
|
||||
levelId: 0,
|
||||
message: `Plugin ${name} gelöscht`
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
return {
|
||||
status: 'delete',
|
||||
pluginName: name,
|
||||
levelId: 4,
|
||||
message: [err.message, err]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async create(name, options = {}) {
|
||||
const pluginPath = path.join(this.pluginBasePath, name);
|
||||
|
||||
if (fs.existsSync(pluginPath)) {
|
||||
return { levelId: 4, pluginName: name, message: `Plugin existiert bereits` };
|
||||
}
|
||||
|
||||
try {
|
||||
const folders = [
|
||||
'views',
|
||||
'views/children',
|
||||
'public/javascript',
|
||||
'public/styles',
|
||||
'public/helpers',
|
||||
'public/images',
|
||||
'public/others',
|
||||
'docs'
|
||||
];
|
||||
|
||||
for (const folder of folders) {
|
||||
await fse.ensureDir(path.join(pluginPath, folder));
|
||||
}
|
||||
|
||||
const files = {
|
||||
'plugin.json': this.__pluginTemplate(name),
|
||||
|
||||
'index.js':
|
||||
`module.exports = class Plugin {
|
||||
constructor(pluginPath, app, service) {
|
||||
this.pluginPath = pluginPath;
|
||||
this.app = app;
|
||||
this.service = service;
|
||||
}
|
||||
}`,
|
||||
|
||||
'routes.js':
|
||||
`module.exports = async (app, pluginPath, metadata, service) => {};`,
|
||||
|
||||
'sockets.js':
|
||||
`module.exports = (app, socketManager, pluginPath, metadata, eventManager) => {};`,
|
||||
|
||||
'docs/help.html':
|
||||
`<h1>Hilfedatei für ${name}</h1><p>${options.description || 'Beschreibung hier einfügen'}</p>`,
|
||||
|
||||
'views/index.hbs':
|
||||
`<div>{{plugin.name}}</div>`
|
||||
};
|
||||
|
||||
for (const [file, content] of Object.entries(files)) {
|
||||
await fse.outputFile(path.join(pluginPath, file), content);
|
||||
}
|
||||
|
||||
this.__setPermissions(pluginPath);
|
||||
|
||||
await this.Plugin.upsert({
|
||||
Name: name,
|
||||
Active: false,
|
||||
Version: options.version || '1.0.0'
|
||||
});
|
||||
|
||||
return {
|
||||
status: 'create',
|
||||
levelId: 0,
|
||||
pluginName: name,
|
||||
message: `Plugin ${name} erstellt`
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
return {
|
||||
status: 'create',
|
||||
levelId: 4,
|
||||
pluginName: name,
|
||||
message: err.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
__pluginTemplate(name, options = { }) {
|
||||
return {
|
||||
name,
|
||||
description: options.description || 'Beschreibung hier einfügen',
|
||||
version: options.version || `1.${new Date().getFullYear().toString().slice(-2)}.${new Date().getMonth() + 1}.${new Date().getDate()}`,
|
||||
menu: {
|
||||
label: name,
|
||||
items:[
|
||||
{
|
||||
label: name,
|
||||
view: "index",
|
||||
defaultSize: { width: '800px', height: '600px' },
|
||||
icon: "../../images/app.png",
|
||||
permissions: ["*"]
|
||||
}
|
||||
]
|
||||
},
|
||||
config: options.config || {},
|
||||
active: true
|
||||
}
|
||||
}
|
||||
|
||||
__setPermissions(pluginPath) {
|
||||
if (!this.filePermissions.user && !this.filePermissions.group) return;
|
||||
|
||||
try {
|
||||
let uid, gid;
|
||||
|
||||
if (this.filePermissions.user) {
|
||||
uid = parseInt(execSync(`id -u ${this.filePermissions.user}`).toString().trim());
|
||||
}
|
||||
|
||||
if (this.filePermissions.group) {
|
||||
gid = parseInt(execSync(`getent group ${this.filePermissions.group} | cut -d: -f3`).toString().trim());
|
||||
}
|
||||
|
||||
const apply = dir => {
|
||||
const entries = fs.readdirSync(dir);
|
||||
|
||||
for (const entry of entries) {
|
||||
const full = path.join(dir, entry);
|
||||
const stat = fs.lstatSync(full);
|
||||
|
||||
fs.chownSync(full, uid, gid);
|
||||
|
||||
if (stat.isDirectory()) apply(full);
|
||||
}
|
||||
};
|
||||
|
||||
apply(pluginPath);
|
||||
|
||||
} catch (err) {
|
||||
throw new Error(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
update(name, updates = {}) {
|
||||
const meta = this.metadata.get(name);
|
||||
|
||||
if (!meta) {
|
||||
return { status: 'update', levelId: 4, message: 'Plugin nicht geladen' };
|
||||
}
|
||||
|
||||
Object.assign(meta.live, updates);
|
||||
|
||||
meta.saveToFile?.();
|
||||
return {
|
||||
status: 'update',
|
||||
pluginName: name,
|
||||
levelId: 0,
|
||||
metadata: meta.live,
|
||||
message: `Plugin aktualisiert ${Object.keys(updates)[0]}: ${Object.values(updates)[0]}`
|
||||
};
|
||||
}
|
||||
|
||||
async rename(oldName, newName) {
|
||||
const oldPath = path.join(this.pluginBasePath, oldName);
|
||||
const newPath = path.join(this.pluginBasePath, newName);
|
||||
const newMetadataPath = path.join(newPath, 'plugin.json');
|
||||
const routesPath = path.join(newPath, 'routes.js');
|
||||
|
||||
const updateRoutesFile = async (filePath, oldName, newName) => {
|
||||
let content = await fse.readFile(filePath, 'utf-8');
|
||||
|
||||
// ersetzt /window/OLDNAME/... → /window/NEWNAME/...
|
||||
const regex = new RegExp(`(app\\.(get|post)\\(['"\`]\\/window\\/)${oldName}(\\/[^'"\`]*['"\`])`, 'g');
|
||||
|
||||
content = content.replace(regex, `$1${newName}$3`);
|
||||
|
||||
await fse.writeFile(filePath, content, 'utf-8');
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// 🔹 Checks
|
||||
if (!this.plugins.has(oldName)) {
|
||||
return {
|
||||
status: 'rename',
|
||||
levelId: 4,
|
||||
message: 'Plugin nicht gefunden'
|
||||
};
|
||||
}
|
||||
|
||||
if (fs.existsSync(newPath)) {
|
||||
return {
|
||||
status: 'rename',
|
||||
levelId: 4,
|
||||
message: 'Neuer Name existiert bereits'
|
||||
};
|
||||
}
|
||||
|
||||
// 🔹 1. copy
|
||||
await fse.copy(oldPath, newPath);
|
||||
|
||||
// 🔹 2. plugin.json fix
|
||||
const metaRaw = this.service.get('fileSystemManager').loadJSON(newMetadataPath);
|
||||
metaRaw.name = newName;
|
||||
metaRaw.menu.label = newName;
|
||||
await fse.writeJSON(newMetadataPath, metaRaw, { spaces: 2 });
|
||||
|
||||
// 🔹 3. routes fix (falls vorhanden)
|
||||
if (fs.existsSync(routesPath)) {
|
||||
await updateRoutesFile(routesPath, oldName, newName);
|
||||
}
|
||||
|
||||
// 🔥 4. ALTES Plugin vorher unloaden
|
||||
await this.unload(oldName);
|
||||
|
||||
// 🔹 5. metadata cleanup
|
||||
this.metadata.delete(oldName);
|
||||
|
||||
// 🔹 6. DB update
|
||||
await this.Plugin.update(
|
||||
{ Name: newName },
|
||||
{ where: { Name: oldName } }
|
||||
);
|
||||
|
||||
// 🔥 7. kurzer delay → verhindert ENOENT durch watcher
|
||||
await new Promise(res => setTimeout(res, 100));
|
||||
|
||||
// 🔹 8. neues Plugin laden
|
||||
await this.load(newName, metaRaw.active);
|
||||
|
||||
// 🔹 9. alten Ordner löschen
|
||||
await fse.remove(oldPath);
|
||||
|
||||
// 🔹 10. altes Plugin löschen
|
||||
this.plugins.delete(oldName);
|
||||
|
||||
return {
|
||||
status: 'rename',
|
||||
pluginName: newName,
|
||||
levelId: 0,
|
||||
message: `Plugin ${oldName} → ${newName} umbenannt`
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
// 🔹 Rollback: neuen Ordner entfernen
|
||||
if (fs.existsSync(newPath)) {
|
||||
await fse.remove(newPath);
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'rename',
|
||||
pluginName: oldName,
|
||||
levelId: 4,
|
||||
message: [err.message, err]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return Array.from(this.plugins.entries()).map(([name, plugin]) => {
|
||||
const meta = this.metadata.get(name);
|
||||
|
||||
return {
|
||||
name,
|
||||
...plugin,
|
||||
...(meta ? meta.live : {})
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
compileHbsTemplate(template) {
|
||||
return this.hbsInstance.handlebars.compile(template);
|
||||
}
|
||||
|
||||
registerPartialsRecursive(dir, prefix = '') {
|
||||
if (!fs.existsSync(dir)) return;
|
||||
|
||||
const files = fs.readdirSync(dir);
|
||||
|
||||
for (const file of files) {
|
||||
const fullPath = path.join(dir, file);
|
||||
const stat = fs.lstatSync(fullPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
this.registerPartialsRecursive(fullPath, path.join(prefix, file));
|
||||
} else if (file.endsWith('.hbs')) {
|
||||
const name = path.join(prefix, path.basename(file, '.hbs')).replace(/\\/g, '/');
|
||||
const template = fs.readFileSync(fullPath, 'utf8');
|
||||
this.hbsInstance.handlebars.registerPartial(name, template);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PluginManager;
|
||||
85
src/services/renderWindow.js
Normal file
85
src/services/renderWindow.js
Normal file
@@ -0,0 +1,85 @@
|
||||
const { service } = require('@root/server.js');
|
||||
|
||||
module.exports = {
|
||||
renderWindow: async function(app, view, data = {}, extraData = {}, res) {
|
||||
const name = res.req.body.name;
|
||||
const label = res.req.body.viewLabel;
|
||||
try {
|
||||
const plugin = app.locals.startMenuItems.find(plugin => plugin.name == name);
|
||||
const windowData = plugin.menu.items.find(item => item.label == label);
|
||||
|
||||
// Alle Daten zusammenführen
|
||||
const templateData = {
|
||||
...{ appname: plugin.menu.label, label: label, section: plugin.section },
|
||||
...windowData,
|
||||
// ...data,
|
||||
...extraData
|
||||
};
|
||||
// console.log(templateData)
|
||||
|
||||
// Zuerst Plugin-View rendern
|
||||
app.render(view, templateData, (err, contentHtml) => {
|
||||
if (err) {
|
||||
service.get('eventManager').write(res.req.cookies.ObjectGUID, 4, name, err);
|
||||
return res.status(500).send(err.message);
|
||||
}
|
||||
// Dann Window-Partial rendern
|
||||
app.render('partials/window', {
|
||||
layout: false,
|
||||
contentHtml,
|
||||
...templateData
|
||||
}, (err, html) => {
|
||||
if (err) {
|
||||
service.get('eventManager').write(res.req.cookies.ObjectGUID, 4, name, err);
|
||||
return res.status(500).send(err.message);
|
||||
}
|
||||
res.status(200).send(html);
|
||||
});
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
service.get('eventManager').write(res.req.cookies.ObjectGUID, 4, name, err);
|
||||
res.status(500).send(err.message);
|
||||
}
|
||||
},
|
||||
renderView: async function(app, view, data = {}, res) {
|
||||
const payload = res.req.body;
|
||||
|
||||
try {
|
||||
|
||||
// console.log(app.locals.startMenuItems.find(name == ).menu.items.find(item => ))
|
||||
// Alle Daten zusammenführen
|
||||
const templateData = {
|
||||
...payload,
|
||||
...{ appname: payload.name, section: 'view' },
|
||||
...{ name: payload.name, view: payload.view, viewLabel: payload.viewLabel},
|
||||
...data,
|
||||
};
|
||||
console.log(templateData)
|
||||
// Zuerst Plugin-View rendern
|
||||
app.render(view, templateData, (err, contentHtml) => {
|
||||
|
||||
if (err) {
|
||||
service.get('eventManager').write(res.req.cookies.ObjectGUID, 4, payload.name, err);
|
||||
return res.status(500).send(err.message);
|
||||
}
|
||||
// Dann Window-Partial rendern
|
||||
app.render('partials/child', {
|
||||
layout: false,
|
||||
contentHtml,
|
||||
...templateData
|
||||
}, (err, html) => {
|
||||
if (err) {
|
||||
service.get('eventManager').write(res.req.cookies.ObjectGUID, 4, payload.name, err);
|
||||
return res.status(500).send(err.message);
|
||||
}
|
||||
res.status(200).send(html);
|
||||
});
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
service.get('eventManager').write(res.req.cookies.ObjectGUID, 4, payload.name, err);
|
||||
res.status(500).send(err.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
336
src/services/socketManager.js
Normal file
336
src/services/socketManager.js
Normal file
@@ -0,0 +1,336 @@
|
||||
class SocketManager {
|
||||
constructor(io) {
|
||||
this.io = io;
|
||||
|
||||
this.namespaces = new Map();
|
||||
this.clients = new Map();
|
||||
|
||||
this.io.on('connection', socket => {});
|
||||
}
|
||||
|
||||
|
||||
add(namespace, exists) {
|
||||
if (this.namespaces.has(namespace)) return this.namespaces.get(namespace);
|
||||
|
||||
const nsp = this.io.of(namespace);
|
||||
const clients = new Map();
|
||||
this.clients.set(namespace, clients);
|
||||
|
||||
nsp.on('connection', socket => {
|
||||
const { objectGuid, sAMAccountName } = socket.handshake.auth || {};
|
||||
if (!objectGuid) return;
|
||||
|
||||
clients.set(objectGuid, {
|
||||
socket,
|
||||
userName: sAMAccountName
|
||||
});
|
||||
|
||||
// console.log(`${sAMAccountName} [${objectGuid}] connected to ${namespace}`);
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
clients.delete(objectGuid);
|
||||
// console.log(`${sAMAccountName} [${objectGuid}] disconnected from ${namespace}`);
|
||||
});
|
||||
});
|
||||
|
||||
this.namespaces.set(namespace, nsp);
|
||||
return nsp;
|
||||
}
|
||||
|
||||
|
||||
async addAsync(namespace, exists) {
|
||||
return new Promise(resolve => {
|
||||
this.add(namespace, exists)
|
||||
|
||||
// simulierter async Init (z. B. DB)
|
||||
setTimeout(resolve, 0);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Broadcast in namespace
|
||||
* @param {string} namespace - Namespace-Name
|
||||
* @param {string} event - Event-Name
|
||||
* @param {any} data - Event-Data
|
||||
*/
|
||||
broadcast(namespace, event, data) {
|
||||
const nsp = this.namespaces.get(namespace);
|
||||
if (nsp) nsp.emit(event, data);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Event-Handler für einen Namespace registrieren
|
||||
* @param {string} namespace - Namespace-Name
|
||||
* @param {string} event - Event-Name
|
||||
* @param {Function} callback - Callback mit (socket, data)
|
||||
*/
|
||||
// on(namespace, event, callback) {
|
||||
// const nsp = this.namespaces.get(namespace);
|
||||
// if (!nsp) {
|
||||
// console.warn(`Namespace ${namespace} doesn't exist`);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// nsp.on('connection', (socket) => {
|
||||
// socket.on(event, (data) => {
|
||||
// // übergibt socket als Kontext, damit sendTo automatisch weiß, wer anfragte
|
||||
// callback(socket, data, (responseEvent, responseData, targetGuid = null) => {
|
||||
// this.sendTo(namespace, targetGuid, responseEvent, responseData, socket);
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
on(namespace, event, callback) {
|
||||
const nsp = this.namespaces.get(namespace);
|
||||
if (!nsp) {
|
||||
console.warn(`Namespace ${namespace} doesn't exist`);
|
||||
return;
|
||||
}
|
||||
|
||||
nsp.on('connection', (socket) => {
|
||||
socket.on(event, (data) => callback(socket, data));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
registerClient(namespace, socket) {
|
||||
const nsp = this.namespaces.get(namespace);
|
||||
if (!nsp) {
|
||||
console.warn(`Namespace ${namespace} does not exist`);
|
||||
return;
|
||||
}
|
||||
|
||||
const auth = socket.handshake.auth;
|
||||
const objectGuid = auth.objectGuid;
|
||||
const sAMAccountName = auth.sAMAccountName;
|
||||
|
||||
if (!objectGuid) {
|
||||
console.warn('Socket has no objectGuid, cannot register');
|
||||
return;
|
||||
}
|
||||
|
||||
socket.customId = objectGuid;
|
||||
|
||||
// In Map speichern
|
||||
this.clients.set(objectGuid, {
|
||||
userName: sAMAccountName,
|
||||
socketId: socket.id,
|
||||
socket: socket
|
||||
});
|
||||
|
||||
console.log(`${sAMAccountName} [${objectGuid}] registered to namespace ${namespace}`);
|
||||
|
||||
// Disconnect-Handler
|
||||
socket.on('disconnect', () => {
|
||||
this.clients.delete(objectGuid);
|
||||
console.log(`${sAMAccountName} [${objectGuid}] disconnected from namespace ${namespace}`);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Entfernt einen Client aus dem Namespace
|
||||
* @param {string} objectGuid - GUID des Clients
|
||||
* @param {string} [namespace] - optional, um Namespace-spezifisch zu loggen
|
||||
*/
|
||||
unregisterClient(objectGuid) {
|
||||
try {
|
||||
const client = this.clients.get(objectGuid);
|
||||
if (!client) return;
|
||||
|
||||
// Optional: Socket trennen
|
||||
client.socket.disconnect();
|
||||
|
||||
this.clients.delete(objectGuid);
|
||||
const name = client.userName;
|
||||
console.log(`${name} [${objectGuid}] manuell entfernt`);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send a personal message to ObjectGUID
|
||||
* @param {string} namespace - Namespace-Name
|
||||
* @param {string} objectGuid - Socket-ID
|
||||
* @param {string} event - Event-Name
|
||||
* @param {any} data - Event-Data
|
||||
* @param {object} [senderSocket] - optionaler Absender-Socket
|
||||
*/
|
||||
sendTo(namespace, objectGuid, event, data, senderSocket = null) {
|
||||
const clients = this.clients.get(namespace);
|
||||
if (!clients) return;
|
||||
|
||||
// Antwort an Absender
|
||||
if (!objectGuid && senderSocket) {
|
||||
senderSocket.emit(event, data);
|
||||
return;
|
||||
}
|
||||
|
||||
const client = clients.get(objectGuid);
|
||||
if (!client?.socket) return;
|
||||
|
||||
client.socket.emit(event, data);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SocketManager;
|
||||
|
||||
// const WebSocket = require("ws");
|
||||
// const url = require("url");
|
||||
|
||||
// class SocketManager {
|
||||
// constructor() {
|
||||
// this.namespaces = new Map(); // "/admin" → { clients:Set, handlers:Map }
|
||||
// this.clients = new Map(); // objectGuid → { userName, socket }
|
||||
|
||||
// // Default-Namespaces wie bei Socket.IO
|
||||
// this.add("/");
|
||||
// this.add("/admin");
|
||||
// }
|
||||
|
||||
|
||||
// /**
|
||||
// * Namespace erstellen
|
||||
// */
|
||||
// add(namespace) {
|
||||
// if (this.namespaces.has(namespace)) return this.namespaces.get(namespace);
|
||||
|
||||
// const ns = {
|
||||
// clients: new Set(),
|
||||
// handlers: new Map() // event → callback
|
||||
// };
|
||||
|
||||
// this.namespaces.set(namespace, ns);
|
||||
// return ns;
|
||||
// }
|
||||
|
||||
|
||||
// /**
|
||||
// * Upgrade (HTTP → WebSocket) pro Namespace auswerten
|
||||
// */
|
||||
// handleUpgrade(namespace, req, socket, head) {
|
||||
// if (!this.namespaces.has(namespace)) {
|
||||
// socket.destroy();
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const ns = this.namespaces.get(namespace);
|
||||
|
||||
// const wss = new WebSocket.Server({ noServer: true });
|
||||
|
||||
// wss.handleUpgrade(req, socket, head, ws => {
|
||||
// this._onConnection(namespace, ws, req);
|
||||
// });
|
||||
// }
|
||||
|
||||
|
||||
// /**
|
||||
// * Verbindung herstellen + authentifizieren
|
||||
// */
|
||||
// _onConnection(namespace, socket, req) {
|
||||
// const params = new URLSearchParams(url.parse(req.url).query);
|
||||
// const objectGuid = params.get("objectGuid");
|
||||
// const sAMAccountName = params.get("sAMAccountName") || "Unknown";
|
||||
|
||||
// socket.namespace = namespace;
|
||||
// socket.objectGuid = objectGuid;
|
||||
// socket.userName = sAMAccountName;
|
||||
|
||||
// // Helper zum JSON-Senden
|
||||
// socket.sendJSON = (obj) => socket.send(JSON.stringify(obj));
|
||||
|
||||
// const ns = this.namespaces.get(namespace);
|
||||
// ns.clients.add(socket);
|
||||
|
||||
// if (objectGuid) {
|
||||
// this.clients.set(objectGuid, { userName: sAMAccountName, socket: socket });
|
||||
// console.log(`${sAMAccountName} [${objectGuid}] connected to [${namespace}]`);
|
||||
// }
|
||||
|
||||
// // Nachrichten-Handling
|
||||
// socket.on("message", raw => {
|
||||
// let msg;
|
||||
// try { msg = JSON.parse(raw); }
|
||||
// catch { return; }
|
||||
|
||||
// const event = msg.event;
|
||||
// const data = msg.data;
|
||||
|
||||
// const cb = ns.handlers.get(event);
|
||||
// if (cb) cb(socket, data);
|
||||
// });
|
||||
|
||||
// // Disconnect
|
||||
// socket.on("close", () => {
|
||||
// ns.clients.delete(socket);
|
||||
|
||||
// if (objectGuid) {
|
||||
// this.clients.delete(objectGuid);
|
||||
// console.log(`${sAMAccountName} [${objectGuid}] disconnected from namespace ${namespace}`);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
|
||||
// /**
|
||||
// * Events registrieren wie socket.on("event")
|
||||
// */
|
||||
// on(namespace, event, callback) {
|
||||
// const ns = this.namespaces.get(namespace);
|
||||
// if (!ns) return console.warn(`Namespace ${namespace} doesn't exist`);
|
||||
// ns.handlers.set(event, callback);
|
||||
// }
|
||||
|
||||
|
||||
// /**
|
||||
// * Broadcast in Namespace
|
||||
// */
|
||||
// broadcast(namespace, event, data) {
|
||||
// const ns = this.namespaces.get(namespace);
|
||||
// if (!ns) return;
|
||||
|
||||
// for (const client of ns.clients) {
|
||||
// if (client.readyState === WebSocket.OPEN) {
|
||||
// client.sendJSON({ event, data });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// /**
|
||||
// * Persönliche Nachricht an GUID
|
||||
// */
|
||||
// sendTo(namespace, objectGuid, event, data, sender = null) {
|
||||
// if (!objectGuid && sender) {
|
||||
// sender.sendJSON({ event, data });
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const client = this.clients.get(objectGuid);
|
||||
// if (!client) return;
|
||||
|
||||
// if (client.socket.readyState === WebSocket.OPEN) {
|
||||
// client.socket.sendJSON({ event, data });
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// /**
|
||||
// * Client manuell entfernen
|
||||
// */
|
||||
// unregisterClient(objectGuid) {
|
||||
// const client = this.clients.get(objectGuid);
|
||||
// if (!client) return;
|
||||
|
||||
// client.socket.close(4000, "manual kick");
|
||||
// this.clients.delete(objectGuid);
|
||||
|
||||
// console.log(`${client.userName} [${objectGuid}] manually removed`);
|
||||
// }
|
||||
// }
|
||||
|
||||
// module.exports = SocketManager;
|
||||
139
src/services/sqlManager.js
Normal file
139
src/services/sqlManager.js
Normal file
@@ -0,0 +1,139 @@
|
||||
const Sequelize = require('sequelize');
|
||||
|
||||
class SqlManager {
|
||||
constructor() {
|
||||
if (SqlManager._instance) return SqlManager._instance;
|
||||
this.instances = {};
|
||||
SqlManager._instance = this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Neue MSSQL Instanz registrieren
|
||||
*/
|
||||
addInstance(name, config) {
|
||||
if (this.instances[name]) {
|
||||
console.log(`[INFO] Instance "${name}" already exists.`);
|
||||
return this.instances[name];
|
||||
}
|
||||
|
||||
const sequelize = new Sequelize(config.database, config.user, config.password, {
|
||||
host: config.host,
|
||||
dialect: 'mssql',
|
||||
port: config.port || 1433,
|
||||
logging: config.logging || false,
|
||||
pool: {
|
||||
max: 10,
|
||||
min: 0,
|
||||
acquire: 30000,
|
||||
idle: 10000
|
||||
},
|
||||
dialectOptions: {
|
||||
options: {
|
||||
encrypt: false,
|
||||
trustServerCertificate: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.instances[name] = { sequelize, config };
|
||||
return sequelize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instanz abrufen
|
||||
*/
|
||||
getInstance(name) {
|
||||
const inst = this.instances[name];
|
||||
if (!inst) {
|
||||
throw new Error(`Instance "${name}" not found.`);
|
||||
}
|
||||
return inst.sequelize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconnect-Logik (falls Verbindung verloren geht)
|
||||
*/
|
||||
async reconnect(name) {
|
||||
console.log(`[WARN] Reconnecting to ${name}...`);
|
||||
|
||||
const old = this.instances[name];
|
||||
if (!old) throw new Error(`Instance "${name}" not found`);
|
||||
|
||||
const { config } = old;
|
||||
|
||||
// alte Verbindung killen
|
||||
try { await old.sequelize.close(); } catch {}
|
||||
|
||||
// neu verbinden
|
||||
const newSequelize = new Sequelize(
|
||||
config.database,
|
||||
config.username,
|
||||
config.password,
|
||||
{
|
||||
...old.sequelize.options
|
||||
}
|
||||
);
|
||||
|
||||
this.instances[name].sequelize = newSequelize;
|
||||
|
||||
try {
|
||||
await newSequelize.authenticate();
|
||||
console.log(`[OK] Reconnected to ${name}`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.log(`[ERROR] Reconnect to ${name} failed:`, err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query ausführen (async)
|
||||
*/
|
||||
async query(instanceName, sql, options) {
|
||||
const sequelize = this.getInstance(instanceName);
|
||||
|
||||
try {
|
||||
const [result] = await sequelize.query(sql, options);
|
||||
return result;
|
||||
} catch (err) {
|
||||
console.error(`[ERROR] Query failed on "${instanceName}":`, err.message);
|
||||
|
||||
if (err.original && err.original.code === "ECONNCLOSED") {
|
||||
await this.reconnect(instanceName);
|
||||
return this.query(instanceName, sql, options);
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pseudo-synchrones Query (Wirkung wie .querySync, intern aber async)
|
||||
* Du wartest darauf, also fühlt es sich synchron an.
|
||||
*/
|
||||
querySync(instanceName, sql, options) {
|
||||
return this.query(instanceName, sql, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verbindung testen
|
||||
*/
|
||||
async test(instanceName) {
|
||||
const sequelize = this.getInstance(instanceName);
|
||||
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
return {
|
||||
levelId: 0,
|
||||
message: `${sequelize.config.database} database connection hergestellt`
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
levelId: 0,
|
||||
message: [`Unable to connect to ${sequelize.config.database}`, error]
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SqlManager;
|
||||
14
src/sockets/adminSocket.js
Normal file
14
src/sockets/adminSocket.js
Normal file
@@ -0,0 +1,14 @@
|
||||
module.exports = (app, socketManager, namespace, eventManager) => {
|
||||
const adminSocket = socketManager.namespaces.get(namespace);
|
||||
|
||||
// socketManager.on(namespace, 'plugin_status', (socket, data) => {
|
||||
// eventManger.write(data.objectGuid, data.levelId, data.pluginName, app.locals.configuration.debug, data.message)
|
||||
// })
|
||||
|
||||
socketManager.on(namespace, 'eventlog', (socket, data) => {
|
||||
eventManager.write(data.objectGuid, data.levelId, data.pluginName, data.message)
|
||||
});
|
||||
|
||||
socketManager.on(namespace, 'heartbeat', () => {
|
||||
})
|
||||
}
|
||||
25
src/sockets/mainSocket.js
Normal file
25
src/sockets/mainSocket.js
Normal file
@@ -0,0 +1,25 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const startMenuItemContext = require('@models/integratedStartmenuItems.js')
|
||||
|
||||
module.exports = (app, socketManager, namespace, pluginManager, authenticationModel, fileSystemManager, eventManager, activeDirectory) => {
|
||||
const mainSocket = socketManager.namespaces.get(namespace);
|
||||
|
||||
socketManager.on(namespace, 'heartbeat', () => setInterval(() => console.log('test'), 1000));
|
||||
|
||||
socketManager.on(namespace, 'event', (socket, data) => {
|
||||
socket.emit('event', { pluginName: data.pluginName, datetime: global.dateFormat(new Date(), 'dd.mm.yyyy HH:MM:SS'), levelId: data.levelId, message: data.message });
|
||||
// eventManager.write(data.objectGuid, data.levelId, data.pluginName, data.message)
|
||||
});
|
||||
|
||||
// global.json.configuration.onChange(info => {
|
||||
// console.log(info.delta)
|
||||
// });
|
||||
|
||||
|
||||
mainSocket.on('connection', socket => {
|
||||
socket.on('changePartial', ({ partial, data }) => {
|
||||
socket.emit('updatePartial', { partial, data });
|
||||
});
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user