initial files
This commit is contained in:
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;
|
||||
Reference in New Issue
Block a user