initial files

This commit is contained in:
2026-04-22 11:55:23 +02:00
commit 92444ff38c
85 changed files with 16324 additions and 0 deletions

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

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

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

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

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

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

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

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

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