Initial commit

This commit is contained in:
2026-08-30 15:23:57 +02:00
commit dab5679098
1293 changed files with 240952 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
# server configuration
INTERNAL_DB_HOST=selfdev-mariadb
INTERNAL_DB_PORT=3306
INTERNAL_DASHBOARD_PORT=3001
# security configuration
INTERNAL_SECRET=53638124b089c718137c6f2a91fa2dfa157c0b440a4dcf695b810fc0dd8f2416
API_SECRET=19cc352caabec690e55575e339ae2011a7724af85b6cf59e4c4a08b166a6cb8a
MASTER_PEPPER=2d49d7e0367bdcbccc6ba063057654121bb41b4fccad8345cb046faab7a9db12
MASTER_SALT=51656
AUTH_SALT=25066
# SMPT information
SMTP_HOST=smtp.strato.de
SMTP_PORT=465
SMTP_USER=noreply@lupinexmedia.de
SMTP_PASS="Gk70mdF3UaRgWKvxbwP5"
+38
View File
@@ -0,0 +1,38 @@
/*
! /configuration/mapping.js
= Central Mapping Configuration
*/
import path from 'path';
import url from 'url';
import dotenv from 'dotenv';
// $ import .env
const __filename = url.fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
dotenv.config({ path: path.resolve(__dirname, '.env') });
// % declare directories
const rootDir = path.resolve(__dirname, '..');
const dirs = {
rootDir: rootDir,
serverDir: path.resolve(rootDir, 'servers'),
// § data structure directories
configDir: path.resolve(rootDir, 'configuration'),
dataDir: path.resolve(rootDir, 'database'),
// § other directories
serviceDir: path.resolve(rootDir, 'services'),
interfaceDir: path.resolve(rootDir, 'ui'),
resourcesDir: path.resolve(rootDir, 'resources')
};
// % set database config
const dbConfig = {
host: process.env.INTERNAL_DB_HOST,
port: process.env.INTERNAL_DB_PORT,
databases: {},
users: {}
};
export { dirs, dbConfig };
+576
View File
@@ -0,0 +1,576 @@
- [Service Manager](#service-manager)
- [Available methods](#available-methods)
- [Available services](#available-services)
- [Usage](#usage)
- [MySQL Service](#mysql-service)
- ["Service Manager" usage](#service-manager-usage)
- [Database \& Role Mapping (Overview)](#database--role-mapping-overview)
- [Using the pools](#using-the-pools)
- [WebSocket Service](#websocket-service)
- ["Service Manager" usage](#service-manager-usage-1)
- [Connection Manager](#connection-manager)
- [Using the WebSocket Connection Manager](#using-the-websocket-connection-manager)
- [Heartbeat system](#heartbeat-system)
- [WebSocket communication format](#websocket-communication-format)
- [Scheduler Service](#scheduler-service)
- ["Service Manager" usage](#service-manager-usage-2)
- [Scheduler Memory](#scheduler-memory)
- [Task structure](#task-structure)
- [Usage](#usage-1)
- [Logging Service](#logging-service)
- ["Service Manager" usage](#service-manager-usage-3)
- [Logger Service Internals](#logger-service-internals)
- [Writing logs](#writing-logs)
- [Read Logs](#read-logs)
- [Authentication](#authentication)
- [Available functions](#available-functions)
- [Usage](#usage-2)
---
## Service Manager
### Available methods
- start
- stop
- restart
- status
### Available services
- mysql
- websocket
- logger
- scheduler
### Usage
**Note:** All service functions are async, use `await` when calling them.
See the documentation of the services themselves below for further information on how to use the functions.
```js
const services = require('./services/manager.js'); // from the ROOT-Directory of the service
// start a service
const result = await services.start.mysql();
console.log(result);
// stop a service
const result = await services.stop.websocket();
console.log(result);
// restart a service
const result = await services.restart.logger();
console.log(result);
// get status of a service
const status = await services.status.scheduler();
console.log(status);
```
---
## MySQL Service
The MySQL service is started via the service manager and provides connection pools for each database, method (read/write/delete/backup) and role (everyone/auth/system).
### "Service Manager" usage
```js
const services = require('./services/manager.js'); // from the ROOT-Directory of the service
// Start the mysql service and get the pools
const mysqlData = await services.start.mysql();
console.log(mysqlData.online); // true if started successfully
const pools = require('./services/database/pools.js').get(); // from the ROOT-Directory of the service
// Stop the mysql service
const stopResult = await services.stop.mysql(pools);
console.log(stopResult.online); // false if all pools were closed
// Restart the mysql service
const restartResult = await services.restart.mysql(pools);
console.log(restartResult.online);
// Check status
const status = await services.status.mysql(pools);
console.log(status);
/* Example output:
{
dbName1: { r: { e: { online: true }, a: { online: true }, s: { online: true } }, ... },
dbName2: { ... }
}
*/
```
### Database & Role Mapping (Overview)
- **Databases**
- name
- name1
- **Methods**
- r = read
- w = write
- d = delete
- b = backup
- **Roles**
- e = everyone
- a = auth
- s = system
> For the full configuration, see `ROOT/service.backend/mapping.js`
### Using the pools
```js
const pools = require('./services/database/pools.js').get(); // from the ROOT-Directory of the service
// Example: read (r) from a table in db "name" with auth (a) role
const pool = pools.name.r.a;
const [rows] = await pool.query('SELECT * FROM users WHERE active = 1');
console.log(rows);
// Example: write (w) to a table in db "name1" with system (s) role
const writePool = pools.name1.w.s;
await writePool.query('INSERT INTO logs (event) VALUES (?)', ['Test']);
// Example: delete (d) rows from a table in db "name" with auth (a) role
const deletePool = pools.name.d.a;
await deletePool.query('DELETE FROM logs WHERE created_at < NOW() - INTERVAL 30 DAY');
// Example: backup (b) method with system (s) role
const backupPool = pools.name1.b.s;
const [backupData] = await backupPool.query('SELECT * FROM important_table');
console.log(backupData);
```
---
## WebSocket Service
The WebSocket service is started via the service manager.
### "Service Manager" usage
```js
const services = require('./services/manager.js'); // from the ROOT-Directory of the service
// Start the websocket service
const wsData = await services.start.websocket();
console.log(wsData.online); // true if started successfully
// Stop the websocket service
const stopResult = await services.stop.websocket();
console.log(stopResult.online);// false if stopped successfully
// Restart the websocket service
const restartResult = await services.restart.websocket();
console.log(restartResult.online);
// Check status
const status = await services.status.websocket();
console.log(status);
/* Example output:
{
online: true,
clients: {
"connectionId1": {
metadata: { connectedAt: 1678321234567 },
lastHeartbeat: 1678321240000
}
}
}
*/
```
**Note**: Restarting the WebSocket service (`services.restart.websocket()`) reloads all actions, including any changes or new files in subfolders.
### Connection Manager
Active WebSocket connections are managed centrally via `services/websocket/connections.js`.
The connection manager:
- Stores all active connections
- Assigns a unique ID to each client
- Tracks heartbeat timestamps
- Automatically removes disconnected or stale clients
- Provides broadcast functionality
This works similarly to the MySQL pool manager:
Connections are stored once and can be accessed globally without restarting the service.
#### Using the WebSocket Connection Manager
```js
const connections = require('./services/websocket/connections.js') // from the ROOT-Directory of the service
// Suppose you have a new WebSocket client "ws"
const clientMetadata = { username: 'Username' };
// 1. Add the client to the manager
const connectionId = connections.add(ws, clientMetadata);
console.log('New connection ID:', connectionId);
// 2. Start the heartbeat mechanism to keep connections alive (every 30 seconds)
connections.startHeartbeat();
// 3. Broadcast a message to all clients
connections.broadcast({ action: 'notify', message: 'Server is running!' });
// 4. Get a specific connection's info
const clientData = connections.get(connectionId);
console.log(clientData);
// 5. Get all active connections
const allClients = connections.getAll();
console.log(allClients);
// 6. Remove a client (e.g., when they disconnect)
connections.remove(connectionId);
// 7. Stop the heartbeat when shutting down the server
connections.stopHeartbeat();
// 8. Clear all connections (e.g., when stopping the WebSocket service)
connections.clearAll();
```
### Heartbeat system
The service automatically sends periodic ping frames to all clients.
If a client does not respond within two intervals, the connection is removed automatically.
This ensures that stale or broken connections are cleaned up without manual intervention.
### WebSocket communication format
After a client connects to the WebSocket server, all communication must follow this message format:
```json
{
action: "",
data: { ... }
}
```
- action: string that matches a loaded action file (ROOT/websocket)
- data: payload passed to the action handler
Example:
```json
{
"action": "user.getUserData", // in this case, we have the action file at 'ROOT/websocket/user/getUserData.js'
"data": { "userId": 42 }
}
```
All the actions are stored in the "websocket" directory of this service. The system automatically loads all .js files inside this folder and all subfolders recursively. Action names are generated from the folder structure using dot-notation.
Example:
- websocket/user/getUserData.js → "user.getUserData"
- websocket/system/internal/restart.js → "system.internal.restart"
Each action file must export an async function:
```js
module.exports = async (data, ws, connectionId) => {
// data = payload from client
// ws = WebSocket instance
// connectionId = internal connection ID
}
// Detailed example:
module.exports = async (data, ws, connectionId) => {
try {
console.log(`TestAction triggered by connection ${connectionId}`);
console.log('Received data:', data);
// set response object
const response = {
message: 'TestAction executed successfully!',
receivedData: data,
timestamp: Date.now(),
connectionId
};
// send response back to the client
ws.send(JSON.stringify({
action: 'test.testaction.response', // optional: own response action
data: response
}));
return response;
} catch (err) {
console.error('Error in TestAction:', err);
ws.send(JSON.stringify({
action: 'test.testaction.error', // optional: own response action
data: { message: err.message }
}));
throw err;
}
};
```
On the client side it looks like this:
```js
// Connect to server
const ws = new WebSocket('ws://localhost:3001');
// Send a message
ws.send(JSON.stringify({
action: 'test.testaction',
data: { foo: 'bar', num: 42 }
}));
// Get a message
ws.onmessage = (msg) => {
const parsed = JSON.parse(msg.data);
console.log('Server response:', parsed);
};
```
---
## Scheduler Service
The Scheduler service is started via the service manager. It manages tasks that can be scheduled to run once at a specific time or repeatedly at defined intervals.
### "Service Manager" usage
```js
const services = require('./services/manager.js'); // from the ROOT-Directory of the service
// Start the scheduler service
const schedulerData = await services.start.scheduler();
console.log(schedulerData.online); // true if started successfully
const tasks = schedulerData.tasks;
// Stop the scheduler service
const stopResult = await services.stop.scheduler();
console.log(stopResult.online); // false if all tasks were stopped
// Restart the scheduler service
const restartResult = await services.restart.scheduler();
console.log(restartResult.online);
// Check status
const status = await services.status.scheduler();
console.log(status);
/* Example output:
{
"taskName1": { active: true, nextRun: 1678325400000, lastRun: 1678321800000, error: null },
"taskName2": { active: false, nextRun: null, lastRun: 1678320000000, error: 'Some error' }
}
*/
```
### Scheduler Memory
The scheduler memory holds all task states centrally. All other modules (start, stop, status) use it to manage tasks.
#### Task structure
```json
{
"taskName": {
"active": true, // whether the task is currently running
"oneTime": false, // true = runs only once, false = repeats
"intervalInSec": 3600, // interval in seconds for repeating tasks
"lastRun": null, // timestamp of last execution
"nextRun": null, // timestamp of next execution
"error": null, // last error if task failed
"action": "taskName" // name of the JS file in ROOT/tasks to execute
}
}
```
**Note**:
- The tasks object in memory contains all loaded tasks.
- The timer property exists internally in memory but is not stored in JSON.
- When a task is active, its timer starts automatically. One-time tasks (oneTime: true) execute once after intervalInSec, while recurring tasks (oneTime: false) repeat indefinitely at the specified interval.
- Each task executes the function exported by its action file in ROOT/tasks. Errors are saved in task.error
- The functions in the tasks folder should be `async` to ensure proper execution of asynchronous operations.
#### Usage
```js
const memory = require('./services/scheduler/memory.js'); // from the ROOT-Directory of the service
// Load all tasks from tasks.json
// Recurring tasks (oneTime: false) are started automatically
// One-time tasks with active: true also start automatically
memory.loadTasks();
// Get a single task
const task = memory.getTask('updater');
console.log(task);
// Get all tasks
const allTasks = memory.getAllTasks();
console.log(allTasks);
// Add or update a task
// If active: true, the timer starts immediately; if oneTime: true, task executes only once after interval
memory.setTask('cleanup', {
active: true,
oneTime: true,
intervalInSec: 10, // in this case, the interval will set delay
action: 'cleanup'
});
// Remove a task
memory.removeTask('cleanup');
```
**Note**:
- The memory object is shared globally. Any changes you make here immediately affect the scheduler service.
- Automatic execution, lastRun/nextRun tracking, and error logging are all handled internally.
---
## Logging Service
The Logging service is started via the service manager. It provides a central way to write log data into the logging database, using the system write pool. The service can be started, stopped, restarted, and its status can be checked. When offline, no log entries are written.
### "Service Manager" usage
```js
const services = require('./services/manager.js'); // from the ROOT-Directory of the service
// Start the logger service
const loggerData = await services.start.logger();
console.log(loggerData.online); // true if started successfully
const tasks = loggerData.tasks;
// Stop the logger service
const stopResult = await services.stop.logger();
console.log(stopResult.online); // false if stopped
// Restart the logger service
const restartResult = await services.restart.logger();
console.log(restartResult.online);
// Check status
const status = await services.status.logger();
console.log(status);
/* Example output:
{ "online": true }
*/
```
### Logger Service Internals
- The logger service maintains a single `online` flag to determine whether logging is active. This is checked before every write to ensure no logs are written while offline.
- When the service is stopped, the `online` flag is set to `false`, preventing any writes to the database.
- Log entries are written dynamically to tables in the `logging` database via the `write()` function.
- The MySQL write pool (`w.s`, `write` method → `system` role) is used internally to insert data.
#### Writing logs
Logs are written using the exported `write()` function from `ROOT/service.backend/services/logger/writeLogs.js`. This function dynamically maps an object of key/value pairs (`logData`) to columns in a specified table. The service must be online for writes to succeed.
```js
const logger = require('./services/logger/writeLogs.js'); // from the ROOT-Directory of the service
module.exports = async (data, ws, connectionId) => {
try {
// Decide internally which table to log to
const tableName = 'access_logs';
// Write log entry (log data can be specified via data.logData or any other object)
const result = await logger.write(data, tableName);
console.log('Log write result:', result);
// Send response back to WebSocket client as needed
} catch (err) {
console.error('Error in TestAction:', err);
throw err;
}
};
```
**Note**:
- If the logger service is offline (`online: false`), the `write()` function returns `{ success: false, error: 'Logger is inactive' }`.
- Columns in `logData` must match the table schema. Values are dynamically inserted into placeholders to prevent SQL injection. Keys not present in the table will be ignored or cause an error depending on DB configuration.
- Any log type (access, error, event, custom) can be handled with the same `write()` function. The table name determines the type.
#### Read Logs
```js
const loggerRead = require('./services/logger/readLogs.js'); // from the ROOT-Directory of the service
// 1. Get all logs from a table
const allLogs = await loggerRead.getAll('access_logs');
console.log(allLogs);
// 2. Get filtered logs from a table
const filteredLogs = await loggerRead.get('access_logs', { user: 'user', action: 'login' });
console.log(filteredLogs);
```
---
## Authentication
The authentication service handles user registration, login, account updates, and email-based account activation. It provides secure password hashing, session management via JWT, and role assignment.
### Available functions
- register
- login
- update
- activate
- check
### Usage
```js
const auth = require('./middleware/auth.js'); // from the ROOT-Directory of the service
// Register a new user
const newUser = await auth.register({
userEmail: 'user@example.com',
userName: 'myUsername',
userPass: 'strongPassword',
userPicture: '/path/to/picture.png', // optional
fromProject: 'myProject'
});
console.log(newUser);
/* Example output:
{
userId: 'uuid-v4',
authKey: 'generated-auth-key',
sessionKey: 'jwt-session-token',
userName: 'myUsername',
userEmail: 'user@example.com',
userPicture: 'uuid-v4.png',
fromProject: 'myProject',
assignedRoles: ['default']
}
*/
// Login
const loggedIn = await auth.login({
userLogin: 'user@example.com', // or username
userPass: 'strongPassword',
sessionKey: null // optional: existing session key for auto-login
});
console.log(loggedIn);
// Update user information
const updatedUser = await auth.update({
userId: loggedIn.userId,
authKey: loggedIn.authKey,
newUserName: 'newUsername', // optional
newUserEmail: 'newEmail@example.com', // optional
newUserPass: 'newPassword', // optional
newUserPicture: '/path/to/new.png', // optional
assignedRoles: ['default', 'activated'], // optional
additionalPermissions: ['admin'], // optional
state: 'active' // optional
});
console.log(updatedUser);
// Activate account (after receiving activation email)
const activation = await auth.activate({
activationToken: '<JWT-TOKEN-FROM-EMAIL>'
});
console.log(activation);
/* Example output:
{
message: 'Account successfully activated.',
assignedRoles: ['default', 'activated']
}
*/
```
**Note**:
- *Passwords* are hashed with bcrypt using a configurable number of salt rounds (`SALT_ROUNDS` in `.env`).
- *Sessions* are managed with JWTs and expire according to `JWT_EXPIRES`.
- *User pictures* are resized to `250x250px` PNGs automatically and stored in the user picture directory.
- *Roles* are stored as JSON arrays in the database and include a default role `'default'` for all new users.
- *Activation* requires the user to click the link sent via email. Once activated, the role `'activated'` is added automatically.
+65
View File
@@ -0,0 +1,65 @@
# Layer -1 ("Layer minus 1") Data Center ([*← Zurück*](../README.md))
Das **Data Center** (im Folgenden als "**L-1**" bezeichnet) ist das Herzstück des **"LNX-Connect"-Ökosystems** (im Folgenden "**LCE**"; "*E*" für "Ecosystem"). **L-1** ist das einzige Modul, das Zugriff auf die Server-interne Datenbank erhält und nur Anfragen vom **Gateway** (im Folgenden "**L0**") akzeptiert und verarbeitet.
---
<h2>Inhaltsverzeichnis</h2>
- [Layer -1 ("Layer minus 1") Data Center (*← Zurück*)](#layer--1-layer-minus-1--data-center--zurück)
- [Definition](#definition)
- [Aufgaben und Verantwortlichkeiten](#aufgaben-und-verantwortlichkeiten)
- [Kommunikation und Zugriffskontrolle](#kommunikation-und-zugriffskontrolle)
- [Datenverarbeitung](#datenverarbeitung)
- [Datenintegrität und Validierung](#datenintegrität-und-validierung)
- [Fehlerbehandlung](#fehlerbehandlung)
- [Module](#module)
---
## Definition
Neben der Datenbank ([*MySQL*](https://www.mysql.com/de/)) ist **L-1** das zentrale Herzstück des **LCE**. Durch eine implementierte Sicherheitsebene mittels [JWT](https://www.npmjs.com/package/jsonwebtoken) sowie weiterer sicherheitsrelevanter Mechanismen wird sichergestellt, dass ausschließlich **L0** Zugriff auf diesen Layer erhält.
## Aufgaben und Verantwortlichkeiten
Da **L-1** der einzige Layer mit Zugriff auf die interne Datenbank ist, besteht die Aufgabe dieses Layers darin, die in der Datenbank gespeicherten Datensätze zu verwalten sowie deren Verarbeitung zu steuern. Die folgenden Aufgabenbereiche gehören dazu:
- Authentifizierung der Anfrage bzw. des Anfragenstellers
- Verarbeitungsschritte nach Authentifizierung:
- Überprüfung der empfangenen Datensätze auf Fehler
- Abgleich der gelieferten Daten mit der Datenbank
- Senden bzw. Schreiben von angefragten oder gelieferten Daten
## Kommunikation und Zugriffskontrolle
**L-1** wird ausschließlich von **L0** über eine interne WebSocket-Verbindung angesprochen, welcher auf `127.0.0.1` lauscht. Dadurch wird sichergestellt, dass kein externer Zugriff direkt auf die Datenbank erfolgen kann. Sämtliche Anfragen müssen den gesicherten Weg über **L0** durchlaufen, wo diese vorab authentifiziert und validiert werden.
Die Kommunikation erfolgt nach einem Request-/Response-Prinzip, wobei **L-1** ausschließlich *reaktiv* arbeitet und keine eigenen Anfragen initiiert.
## Datenverarbeitung
Die Datenverarbeitung in **L-1** erfolgt strikt nach einem definierten Ablauf. Jede eingehende Anfrage wird zunächst auf Gültigkeit geprüft und anschließend entsprechend ihres Typs verarbeitet. Dabei wird zwischen Lese-, Schreib- und Löschoperationen (*CRUD*; Create, Read, Update, Delete) unterschieden.
Nach erfolgreicher Authentifizierung und Validierung werden die angeforderten Operationen auf der Datenbank ausgeführt. Anschließend wird eine entsprechende Antwort generiert und an **L0** zurückgesendet, welcher die Antwort dann an den jeweiligen Client weiterleitet. Dieser strukturierte Ablauf stellt sicher, dass all Datenoperationen kontrolliert und nachvollziehbar durchgeführt werden.
## Datenintegrität und Validierung
**L-1** stellt sicher, dass alle verarbeiteten Daten konsistent und fehlerfrei bleiben. Eingehende Datensätze werden vor der Verarbeitung auf Struktur, Vollständigkeit und mögliche Fehler überprüft.
Durch den Abgleich mit bestehenden Daten in der Datenbank wird verhindert, dass inkonsistente oder widersprüchliche Informationen gespeichert oder ausgegeben werden. Diese Validierungsmechanismen gewährleisten die Integrität der gesamten Datenbasis innerhalb des Systems.
## Fehlerbehandlung
Tritt während der Verarbeitung einer Anfrage ein Fehler auf, wird dieser von **L-1** erkannt und entsprechend behandelt. Ungültige oder fehlerhafte Anfragen werden nicht verarbeitet, sondern mit einer passenden Fehlermeldung beantwortet.
Dabei wird sichergestellt, dass keine inkonsistenten Zustände in der Datenbank entstehen. Fehler werden zudem intern erfasst, um eine spätere Analyse und Optimierung des Systems zu ermöglichen.
## Module
**L-1** ist in mehrere Module unterteilt, die jeweils spezifische Aufgaben innerhalb der Datenverarbeitung übernehmen:
- **Service-Manager-Modul** — Koordination der internen Module → [Mehr](./modules/service-manager.md)
- **WebSocket-Modul** — Interne sichere WebSocket-Kommunikation → [Mehr](./modules/websocket-service.md)
- **DB-Service-Modul** — Datenbank-Manager und Pools → [Mehr](./modules/database-service.md)
- **Auth-Modul** — Anfrageauthentifizierung → [Mehr](./modules/auth-service.md)
- **Validierungsmodul** — Datenstruktur- und Konsistenzprüfung → [Mehr](./modules/validate-service.md)
- **Datenverarbeitungsmodul** — Datenbank-Operations-Manager → [Mehr](./modules/operations-manager.md)
- **Loggingmodul** — Systemereignisse und Fehler protokollieren → [Mehr](./modules/logging-service.md)
+24
View File
@@ -0,0 +1,24 @@
/*
! /index.js
= Central Project Entry-Point
*/
// % import node & npm modules
import path from 'path';
import { createServer } from 'http';
// % import modules
import { dirs, dbConfig } from './configuration/mapping.js';
import { generate, generatePrefixed } from './services/idGenerator.js';
// % set neccessary directories and paths
const expressDir = path.resolve(dirs.serverDir, 'express');
const webSocketDir = path.resolve(dirs.serverDir, 'websocket');
async function startConnect() {
const express = await import(`${expressDir}/server.js`);
const websocket = await import(`${webSocketDir}/server.js`);
const server = createServer(express.default);
}
startConnect();
+55
View File
@@ -0,0 +1,55 @@
{
"name": "lnx-connect",
"version": "1.0.0",
"description": "",
"keywords": [
"connect",
"eco",
"system",
"system",
"lnx",
"control"
],
"homepage": "https://github.com/LupiNexMedia/lnx-connect#readme",
"bugs": {
"url": "https://github.com/LupiNexMedia/lnx-connect/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/LupiNexMedia/lnx-connect.git"
},
"license": "UNLICENSED",
"author": "LupiNex Media",
"type": "commonjs",
"main": "index.js",
"scripts": {
"test": "nodemon index.js"
},
"dependencies": {
"bcrypt": "^6.0.0",
"body-parser": "^2.2.2",
"browser-sync": "^3.0.4",
"chalk": "^5.6.2",
"chokidar": "^5.0.0",
"clean-css": "^5.3.3",
"cookie-parser": "^1.4.7",
"cors": "^2.8.6",
"crypto": "^1.0.1",
"dotenv": "^17.4.0",
"ejs": "^5.0.1",
"express": "^5.2.1",
"express-minify": "^1.0.0",
"express-minify-html-2": "^2.0.0",
"fs": "^0.0.1-security",
"jsonwebtoken": "^9.0.3",
"mysql2": "^3.20.0",
"nodemailer": "^8.0.4",
"nodemon": "^3.1.14",
"os": "^0.1.2",
"path": "^0.12.7",
"sharp": "^0.34.5",
"uglify-js": "^3.19.3",
"uuid": "^13.0.0",
"ws": "^8.20.0"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.3 MiB

@@ -0,0 +1,21 @@
{
"name": "LupiNex Connect",
"short_name": "LupiNex Connect",
"icons": [
{
"src": "/img/favicons/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/img/favicons/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 981 KiB

+49
View File
@@ -0,0 +1,49 @@
/*
! /servers/Dashboard/functions.js Central Dashboard Server Functions
? This modules exports the functionality for the express dashboard server
*/
// % core variables
const ip = process.env.INTERNAL_HOST_IP;
const port = process.env.INTERNAL_DASHBOARD_PORT;
// $ server start function
async function boot(server) {
// ~ start express server
server.listen(port, ip, () => {
console.info(`Express server is now running on http://${ip}:${port}`)
});
// ~ return running express server reference
return server;
};
// $ server shutdown function
async function shutdown(server) {
// ~ shutdown running express server
server.close();
// ~ return express server reference
return server;
};
// $ server restart function
async function restart(server) {
// ~ shutdown the running express server
await shutdown(server);
// ~ restart the express server
await boot(server);
// ~ return the server reference
return server;
};
// $ server status function
async function status(server) {
// ~ get and return server listening status
return server.listening ? 'online' : 'offline';
};
// § export server functions module
module.exports = { boot, shutdown, restart, status };
@@ -0,0 +1,32 @@
/*
! /servers/dashboard/routers/index.js Central Gateway Routing Aggregator
? This module dynamically collects and registeres all modular ".router.js" files within the "routers" directory
*/
// % import nodejs packages
const router = require('express').Router();
const path = require('path');
const fs = require('fs');
// % import external dependencies
const rootDir = path.resolve(__dirname, '..', '..', '..');
const { dirs } = require(path.resolve(rootDir, 'configuration', 'mapping.js'));
// % read router files recursively
const routerRoot = path.resolve(dirs.dashboardDir, 'routers');
const routerFiles = fs.readdirSync(routerRoot)
.filter(file => /\.router\.js$/.test(file))
.sort((a, b) => {
if (a === 'root.router.js') return 1;
if (b === 'root.router.js') return -1;
return a.localeCompare(b);
});
// $ configure used routers
routerFiles.forEach(file => {
const routerPath = path.resolve(routerRoot, file);
router.use(require(routerPath));
});
// § export router module
module.exports = router;
+35
View File
@@ -0,0 +1,35 @@
/*
! /servers/dashboard/server.js Central Dashboard Server Manager
? This module orchestrates the express lifecycle, handles initialization, middleware configs and router integrations.
*/
// % import nodejs packages
const express = require('express');
const http = require('http');
const path = require('path');
// % import external dependencies
const rootDir = path.resolve(__dirname, '..', '..');
const { dirs } = require(path.resolve(rootDir, 'configuration', 'mapping.js'));
// % set up express server
const app = express();
const server = http.createServer(app);
// $ server setup
app.use(express.json()); // add json support for express
app.use(express.static(path.resolve(dirs.interfaceDir, 'resources'))); // set resources directory (e.g. scripts, style sheets, images ...)
app.set('view engine', 'ejs'); // set "embedded javascript" as view engine
app.set('views', path.resolve(dirs.interfaceDir, 'views')); // set the folder where the .ejs files are located
// $ router setup
const router = require(path.resolve(dirs.dashboardDir, 'routers'));
app.use(router);
// $ server functions
const {
boot, shutdown, restart, status
} = require(path.resolve(dirs.dashboardDir, 'functions.js'));
// $ export server module
module.exports = { boot, shutdown, restart, status, server };
View File
+93
View File
@@ -0,0 +1,93 @@
/*
! /servers/gateway/functions.js Central Gateway Server Functions
? This modules exports the functionality for the express gateway server
*/
// % import nodejs dependencies
const WebSocket = require('ws');
// % core variables
const ip = process.env.INTERNAL_HOST_IP;
const port = process.env.INTERNAL_GATEWAY_PORT;
// % create a reference for the websocket client
let wsClient = null;
// % create an event emitter for the gateway
const EventEmitter = require('events');
const gatewayEvents = new EventEmitter();
// $ helper: send messages through websocket
function sendToWs(payload) {
if (wsClient && wsClient.readyState === WebSocket.OPEN) {
wsClient.send(JSON.stringify(payload));
} else {
console.error('WebSocket not connected.');
}
}
// $ server start function
async function boot(server) {
// ~ start express server
server.listen(port, ip, () => {
console.info(`Express server is now running on http://${ip}:${port}`);
});
// ~ connect to the websocket server
const wsUrl = `ws://${process.env.INTERNAL_HOST_IP}:${process.env.INTERNAL_WEBSOCKET_PORT}?key=${process.env.INTERNAL_SECRET}&codename=gateway`;
wsClient = new WebSocket(wsUrl);
wsClient.on('open', () => {
console.info('Connected to central websocket server');
});
wsClient.on('message', (data) => {
const msg = JSON.parse(data);
gatewayEvents.emit('ws_message', msg);
});
wsClient.on('error', (err) => {
console.error('WebSocket error:', err.message);
});
// ~ return running express server reference
return server;
};
// $ server shutdown function
async function shutdown(server) {
// ~ close websocket server
if (wsClient) {
wsClient.close();
wsClient = null;
}
// ~ shutdown running express server
return new Promise((resolve) => {
server.close(() => {
console.info('Express server shut down.');
resolve(server);
});
});
};
// $ server restart function
async function restart(server) {
// ~ shutdown the running express server
await shutdown(server);
// ~ restart the express server
await boot(server);
// ~ return the server reference
return server;
};
// $ server status function
async function status(server) {
// ~ get and return server listening status
return server.listening ? 'online' : 'offline';
};
// § export server functions module
module.exports = { boot, shutdown, restart, status, sendToWs, gatewayEvents };
+32
View File
@@ -0,0 +1,32 @@
/*
! /servers/gateway/routers/index.js Central Gateway Routing Aggregator
? This module dynamically collects and registeres all modular ".router.js" files within the "routers" directory
*/
// % import nodejs packages
const router = require('express').Router();
const path = require('path');
const fs = require('fs');
// % import external dependencies
const rootDir = path.resolve(__dirname, '..', '..', '..');
const { dirs } = require(path.resolve(rootDir, 'configuration', 'mapping.js'));
// % read router files recursively
const routerRoot = path.resolve(dirs.gatewayDir, 'routers');
const routerFiles = fs.readdirSync(routerRoot)
.filter(file => /\.router\.js$/.test(file))
.sort((a, b) => {
if (a === 'root.router.js') return 1;
if (b === 'root.router.js') return -1;
return a.localeCompare(b);
});
// $ configure used routers
routerFiles.forEach(file => {
const routerPath = path.resolve(routerRoot, file);
router.use(require(routerPath));
});
// § export router module
module.exports = router;
@@ -0,0 +1,30 @@
/*
! /servers/gateway/routers/template.router.js
*/
// % import nodejs dependencies
const router = require('express').Router();
// % import external dependencies
const rootDir = path.resolve(__dirname, '..', '..', '..');
const { dirs } = require(path.resolve(rootDir, 'configuration', 'mapping.js'));
const { gatewayEvents, sendToWs } = path.resolve(dirs.gateway, 'functions.js');
// $ template router with websocket integration
router.post('/test', (req, res) => {
const correlationId = '';
sendToWs({
type: 'actionName',
correlationId,
data: req.body
});
// ~ wait for callback through central event bus
gatewayEvents.once('ws_message', (msg) => {
if (msg.correlationId === correlationId) res.json(msg.data);
});
});
// § export routers
module.exports = router;
+32
View File
@@ -0,0 +1,32 @@
/*
! /servers/gateway/server.js Central Gateway Server Manager
? This module orchestrates the express lifecycle, handles initialization, middleware configs and router integrations
*/
// % import nodejs packages
const express = require('express');
const http = require('http');
const path = require('path');
// % import external dependencies
const rootDir = path.resolve(__dirname, '..', '..');
const { dirs } = require(path.resolve(rootDir, 'configuration', 'mapping.js'));
// % set up express server
const app = express();
const server = http.createServer(app);
// $ server setup
app.use(express.json()); // add json support for express
// $ router setup
const router = require(path.resolve(dirs.gatewayDir, 'routers'));
app.use(router);
// $ server functions
const {
boot, shutdown, restart, status
} = require(path.resolve(dirs.gatewayDir, 'functions.js'));
// $ export server module
module.exports = { boot, shutdown, restart, status, server };
@@ -0,0 +1,8 @@
/*
! /servers/websocket/actions/template.js A simple websocket action template
? This module is just a template for the websocket actions
*/
module.exports = (connectionManager, data, id) => {
};
+185
View File
@@ -0,0 +1,185 @@
/*
! /servers/websocket/connections.js Central WebSocket Connection Manager
? This module handles all websocket connections as a pool and automatically sends heartbeats
*/
// % import nodejs dependencies
const path = require('path');
// % import external dependencies
const rootDir = path.resolve(__dirname, '..', '..');
const { generatePrefixed } = require(path.resolve(rootDir, 'services', 'idgen.js'));
// % create a new connection mapping
let connections = new Map();
let registry = new Map();
// % set heartbeat settings
let heartbeatInterval = null;
const heartbeatTime = 5000; // 5 seconds
// $ add a connection
function add(socket, codename = null) {
// % generate a unique id for websocket connections
const id = generatePrefixed('ws');
// ~ add connection to connection pool
const conn = { socket, codename, lastHeartbeat: Date.now() };
connections.set(id, conn);
// ~ add registry keys if codename is provided
if (codename) registry.set(codename, id);
// ~ listen for 'pong' events from client to update heartbeat timestamp
socket.on('pong', () => conn.lastHeartbeat = Date.now());
// ~ error handling
socket.on('error', (err) => remove(id));
// ~ close connection
socket.on('close', () => remove(id));
// ~ return client id
return id;
}
// $ remove a connection by id
function remove(id) {
// % find id in connection map
const conn = connections.get(id);
// ~ close and remove connection
if (conn) {
// ~ cleanup registry if codename exists
if (conn.codename) registry.delete(conn.codename);
// ~ close connection
if (conn.socket.readyState === conn.socket.OPEN) conn.socket.close();
// ~ remove connection from pool
connections.delete(id);
}
}
// $ remove all connections
function removeAll() {
// ~ loop through connection mapping
for (const id of connections.keys()) {
remove(id);
}
}
// $ get a connection by id
function get(id) {
return connections.get(id);
}
// $ get a connection by codename
function getByCodename(codename) {
// % get id from codename
const id = registry.get(codename);
// ~ return connection
return id ? connections.get(id) : null;
}
// $ get all connections
function getAll() {
// % create a reference for all connections
const all = {};
// ~ loop through connections and fill "all" reference
for (const [id, data] of connections.entries()) {
all[id] = {
codename: data.codename,
lastHeartbeat: data.lastHeartbeat
};
}
// ~ return the connection reference
return all;
}
// $ send a message to a specific connection by id
function send(id, message) {
// % get id
const conn = connections.get(id);
// ~ check if connection exists and is open
if (conn && conn.socket.readyState === conn.socket.OPEN) {
conn.socket.send(JSON.stringify(message));
return true;
}
// ~ return immediately after a failure
return false;
}
// $ send a message to a specific connection by codename
function sendTo(codename, message) {
// % get id
const id = registry.get(codename);
// ~ send a message if id exists
if (id) return send(id, message);
// ~ immediately return if id not exists
return false;
}
// $ broadcast to all connections
function broadcast(message) {
// % serialize payload
const payload = JSON.stringify(message);
// ~ loop through all established connections
for (const conn of connections.values()) {
if (conn.socket.readyState === conn.socket.OPEN) {
conn.socket.send(payload);
}
}
}
// $ start the heartbeat system
function startBeat() {
if (heartbeatInterval) return;
heartbeatInterval = setInterval(() => {
const now = Date.now();
for (const [id, conn] of connections.entries()) {
// ~ send ping to active connections
if (conn.socket.readyState === conn.socket.OPEN) {
conn.socket.ping();
}
// ~ remove stale connections (no pong in 2 heartbeat intervals)
if (now - conn.lastHeartbeat > heartbeatTime * 2) {
remove(id);
}
}
}, heartbeatTime);
}
// $ stop the heartbeat system
function stopBeat() {
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
heartbeatInterval = null;
}
}
// § export the websocket connection module
module.exports = {
add,
remove,
removeAll,
get,
getByCodename,
getAll,
send,
sendTo,
broadcast,
startBeat,
stopBeat
};
+134
View File
@@ -0,0 +1,134 @@
/*
! /servers/websocket/server.js Central Websocket Server Manager
? This module orchestrates the websocket lifecycle, handles connections, heartbeats and actions
*/
// % import nodejs packages
const path = require('path');
const fs = require('fs');
const http = require('http');
const WebSocket = require('ws');
// % import external dependencies
const rootDir = path.resolve(__dirname, '..', '..');
const { dirs } = require(path.resolve(rootDir, 'configuration', 'mapping.js'));
const connectionManager = require(path.resolve(dirs.websocketDir, 'connections.js'));
// % declare action directories
const wsActionsDir = path.resolve(dirs.websocketDir, 'actions');
// % declare reference for actions
const actions = {};
// % declare references for the websocket server
const ip = process.env.INTERNAL_HOST_IP;
const port = process.env.INTERNAL_WEBSOCKET_PORT;
let server = null;
// $ load all websocket actions recursively
// ? The loader is loading all actions with a dot notation
// ?? Usage: actionName || folder.actionName || folder.folder2.actionName ....
async function loadActions(dir = wsActionsDir, prefix = '') {
// ~ reset the action container to ensure a clean state during reloads
for (const key in actions) delete actions[key];
// % read all content synchronously
const files = fs.readdirSync(dir);
// ~ loop through each file or folder found in the directory
for (const file of files) {
// % construct the absolute path to the current item
const fullPath = path.resolve(dir, file);
// ~ retrieve file system metadata
const stat = fs.statSync(fullPath);
// ~ check if item is a folder
if (stat.isDirectory()) {
// ~ recurse into subdirs and append the folder name to the prefix
loadActions(fullPath, prefix + file + '.');
} else if (file.endsWith('.js')) {
// ~ generate a unique action name by stripping the file extension and include the prefix
const actionName = prefix + file.replace('.js', '');
// ~ invalidate the require cache to allow loading the updated code
delete require.cache[require.resolve(fullPath)];
// ~ load the fresh module version into the actions registry
actions[actionName] = require(fullPath);
}
}
}
// $ function to start the websocket server
async function start() {
// ~ initially load all websocket actions
await loadActions();
try {
// ~ set up a new websocket server
const wss = new WebSocket.Server({
host: ip,
port: port
});
wss.on('connection', async (socket, req) => {
// ~ authorize connection
const url = new URL(req.url, `http://${req.headers.host}`);
const key = url.searchParams.get('key');
const codename = url.searchParams.get('codename');
if (key !== process.env.INTERNAL_SECRET) {
socket.close(1008, 'Unauthorized');
return;
}
// ~ get connection id and establish connection
const id = await connectionManager.add(socket, codename);
connectionManager.sendTo(codename, id);
// ~ register websocket messages
socket.on('message', async (data) => {
// ~ parse json data
const msg = JSON.parse(data);
// % get user id and validate
const valid = connectionManager.get(id);
if (valid.socket.readyState !== valid.socket.OPEN) {
connectionManager.remove(id);
return;
};
// % declare action type (e.g. folder.actionName)
const type = msg.type;
if (!type) {
connectionManager.send(id, 'Action value is empty. Please declare an action value.');
return;
};
// ~ set action handler
const actionHandler = actions[msg.type];
if (!actionHandler) {
connectionManager.send(id, `Action ${actionHandler} not found.`);
return;
}
// ~ send action to handler
try {
await actionHandler(connectionManager, msg.data, id);
} catch(e) {
console.error('Error while trying to execute the websocket action!', e);
return;
}
});
});
// ~ start the heartbeat system with the connection manager
connectionManager.startBeat();
} catch(e) {
console.error('Error while trying to start the websocket server', e);
connectionManager.stopBeat();
}
}
// § export websocket server and functions
module.exports = { start, loadActions };
@@ -0,0 +1,51 @@
// node dependencies
const nodemailer = require('nodemailer');
require('dotenv').config({ path: '../.env' });
async function sendStratoMail({ to, subject, html }) {
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: parseInt(process.env.SMTP_PORT),
secure: true,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
}
});
await transporter.sendMail({
from: `"LupiNex Media" <${process.env.SMTP_USER}>`,
to,
subject,
html
});
}
module.exports = {
sendMail: async (userId, userName, userEmail) => {
// 1. Generate activation token (signed JWT)
const activationToken = jwt.sign({ user_id: userId }, JWT_SECRET, { expiresIn: '7d' });
// 2. Build activation URL
const activationLink = `${process.env.FRONTEND_URL}/activate?token=${activationToken}`;
// 3. Build email content
const subject = 'Please activate your account';
const html = `
<p>Hello ${userName},</p>
<p>Thank you for your registration. Please click the link below to activate your account:</p>
<p><a href="${activationLink}">Activate Account</a></p>
<p>The link is valid for 7 days.</p>
`;
// 4. Send email via Strato SMTP
await sendStratoMail({
to: userEmail,
subject,
html
});
return { message: 'Activation mail sent.' };
}
}
+384
View File
@@ -0,0 +1,384 @@
// node dependencies
const { v4: uuidv4 } = require('uuid');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const sharp = require('sharp');
const fs = require('fs');
const path = require('path');
require('dotenv').config({ path: '../.env' });
// variables
const SALT_ROUNDS = parseInt(process.env.SALT_ROUNDS);
const JWT_SECRET = process.env.JWT_SECRET;
const JWT_EXPIRES = '30d';
// file paths
const USER_PIC_DIR = '/mnt/openarchive/.lnx_connect/userPictures';
const TMP_UPLOAD_DIR = '/mnt/openarchive/.lnx_connect/tmp';
// dependencies
const pools = require('../services/database/pools');
const { sendMail } = require('./activator');
// helper
async function saveUserPicture(userId, tmpFilePath) {
if (!fs.existsSync(USER_PIC_DIR)) fs.mkdirSync(USER_PIC_DIR, { recursive: true });
const destPath = path.join(USER_PIC_DIR, `${userId}.png`);
if (tmpFilePath && fs.existsSync(tmpFilePath)) {
// resize uploaded image
await sharp(tmpFilePath)
.resize(250, 250)
.png({ compressionLevel: 9 })
.toFile(destPath);
// delete tmp file
fs.unlinkSync(tmpFilePath);
return `${userId}.png`
} else {
return `default.png`;
}
}
// export functions
module.exports = {
// register function
register: async (data) => {
// 0. Declare dbs to use
const read = pools.users.r.s;
const write = pools.users.w.s;
// 1. Get incoming variables
const {
userEmail: user_email,
userName: user_name,
userPass: user_pass,
userPicture: user_picture,
fromProject: from_project
} = data;
// 2. Check if email or username already exists
const existing = await read.query(`
SELECT user_email, user_name
FROM users
WHERE user_email = ? OR user_name = ?
`, [user_email, user_name]);
// If exists throw error
if (existing.length > 0) {
const existingUser = existing[0];
if (existingUser.user_email === user_email) throw new Error('User with that mail already exists.');
if (existingUser.user_name === user_name) throw new Error('User with that name already exists.');
}
// 3. Generate IDs and keys
const user_id = uuidv4();
const authKeyPlain = uuidv4();
const auth_key = await bcrypt.hash(authKeyPlain, SALT_ROUNDS);
const pass_hash = await bcrypt.hash(user_pass, SALT_ROUNDS);
const session_key = jwt.sign({ user_id }, JWT_SECRET, { expiresIn: JWT_EXPIRES });
// 4. Handle user picture
const tmpFileName = path.basename(user_picture || '');
const tmpFilePath = path.join(TMP_UPLOAD_DIR, tmpFileName);
const user_picture_file = await saveUserPicture(user_id, tmpFilePath);
// 5. Set default role
const default_role = ['default'];
// 5. Write data to db
await write.query(`
INSERT INTO users (
user_id,
auth_key,
session_key,
user_name,
user_email,
user_pass,
user_picture,
from_project,
assigned_roles,
state,
last_active
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
`, [
user_id,
auth_key,
session_key,
user_name,
user_email,
pass_hash,
user_picture_file,
from_project,
JSON.stringify(default_role),
'active'
]);
// send activation mail
sendMail(user_id, user_name, user_email);
// Objectize result
const result = {
userId: user_id,
authKey: authKeyPlain,
sessionKey: session_key,
userName: user_name,
userEmail: user_email,
userPicture: user_picture_file,
fromProject: from_project,
assignedRoles: default_role
}
// Return result
return result;
},
// login function
login: async (data) => {
// Dummy hash used to normalize login timing
const DUMMY_HASH = '$2b$10$nC7jBLRF/HwpZLkt79xCreF5Npsh5NeVFnGzOrRF7sVxlI9yJk99i';
// 0. Declare dbs to use
const read = pools.users.r.s;
const write = pools.users.w.s;
// 1. Get incoming variables
const {
userLogin: user_login,
userPass: user_pass,
sessionKey: session_key
} = data;
const login = (user_login || '').trim(); // trim login name/mail
// 2. Check if user exists
const existing = await read.query(`
SELECT
user_id,
auth_key,
session_key,
user_name,
user_email,
user_pass,
user_picture,
from_project,
assigned_roles,
additional_permissions,
state
FROM users
WHERE user_email = ? OR user_name = ?
`, [login, login]);
let user = existing[0];
// Run fake bcrypt compare to match timing
if (!user) {
await bcrypt.compare(user_pass || '', DUMMY_HASH);
throw new Error('Invalid login credentials.');
}
// 3. Check user state
if (user.state === 'banned' || user.state === 'pending') throw new Error(`User ${user.user_name} is ${user.state}.`);
// 4. Login validation
let loginValid = false;
let autoLogin = false;
// 4.1. Session login (auto login)
if (session_key) {
try {
const decoded = jwt.verify(session_key, JWT_SECRET);
if (decoded.user_id === user.user_id && session_key === user.session_key) {
loginValid = true;
autoLogin = true;
};
} catch (err) {
loginValid = false;
autoLogin = false;
}
}
// 4.2. Password login
let new_session_key;
if (!loginValid) {
// match passwords
const passMatch = await bcrypt.compare(user_pass || '', user.user_pass);
if (!passMatch) throw new Error('Invalid login credentials.');
loginValid = true;
// generate new session key
new_session_key = jwt.sign({ user_id: user.user_id }, JWT_SECRET, { expiresIn: JWT_EXPIRES });
}
const newAuthKeyPlain = uuidv4();
const newAuthKeyHash = await bcrypt.hash(newAuthKeyPlain, SALT_ROUNDS);
// 6. Update session after login
if (loginValid && !autoLogin) {
await write.query(`
UPDATE users
SET auth_key = ?, session_key = ?, last_active = NOW()
WHERE user_id = ?
`, [newAuthKeyHash, new_session_key, user.user_id]);
}
if (loginValid && autoLogin) {
await write.query(`
UPDATE users
SET auth_key = ?, last_active = NOW()
WHERE user_id = ?
`, [newAuthKeyHash, user.user_id]);
}
// Objectize result
const result = {
userId: user.user_id,
authKey: autoLogin ? user.auth_key : newAuthKeyPlain,
sessionKey: autoLogin ? user.session_key : new_session_key,
userName: user.user_name,
userEmail: user.user_email,
userPicture: user.user_picture,
fromProject: user.from_project,
assignedRoles: user.assigned_roles ? JSON.parse(user.assigned_roles) : [],
additionalPermissions: user.additional_permissions ? JSON.parse(user.additional_permissions) : []
}
// Return result
return result;
},
// update function
update: async (data) => {
// 0. Declare dbs to use
const read = pools.users.r.s;
const write = pools.users.w.s;
// 1. Get incoming variables
const {
userId: user_id,
authKey: auth_key,
newUserName: user_name,
newUserEmail: user_email,
newUserPass: user_pass,
newUserPicture: user_picture,
assignedRoles: assigned_roles,
additionalPermissions: additional_permissions,
state: new_state
} = data;
// 2. Fetch current user
const existing = await read.query(`
SELECT
user_id,
auth_key,
user_name,
user_email,
user_pass,
user_picture,
assigned_roles,
additional_permissions,
state
FROM users
WHERE user_id = ?
`, [user_id]);
if (!existing[0]) throw new Error('User not found.');
const user = existing[0];
// 3. Check auth key
const authMatch = await bcrypt.compare(auth_key, user.auth_key);
if (!authMatch) throw new Error('Invalid auth key.');
// 4. Prepare fields to update
const updates = {};
if (user_name && user_name !== user.user_name) updates.user_name = user_name;
if (user_email && user_email !== user.user_email) updates.user_email = user_email;
if (user_pass) updates.user_pass = await bcrypt.hash(user_pass, SALT_ROUNDS);
if (assigned_roles) updates.assigned_roles = JSON.stringify(assigned_roles);
if (additional_permissions) updates.additional_permissions = JSON.stringify(additional_permissions);
if (new_state && new_state !== user.state) updates.state = new_state;
// 5. Handle new user picture
if (user_picture) {
const tmpFileName = path.basename(user_picture || '');
const tmpFilePath = path.join(TMP_UPLOAD_DIR, tmpFileName);
updates.user_picture = await saveUserPicture(user_id, tmpFilePath);
}
// 6. Build dynamic update query
const fields = Object.keys(updates);
if (fields.length === 0) return { message: 'Nothing to update.' };
const placeholders = fields.map(f => `${f} = ?`).join(', ');
const values = fields.map(f => updates[f]);
values.push(user_id);
await write.query(`
UPDATE users
SET ${placeholders}, last_active = NOW()
WHERE user_id = ?
`, values);
// 7. Return updated user data
const updatedUser = await read.query(`
SELECT
user_id,
user_name,
user_email,
user_picture,
from_project,
assigned_roles,
additional_permissions,
state
FROM users
WHERE user_id = ?
`, [user_id]);
return updatedUser[0];
},
// activation function
activate: async (data) => {
// 0. Declare dbs to use
const read = pools.users.r.s;
const write = pools.users.w.s;
// 1. Get incoming token
const { activationToken } = data;
if (!activationToken) throw new Error('No activation token provided.');
let payload;
try {
// 2. Verify token
payload = jwt.verify(activationToken, JWT_SECRET);
} catch (err) {
throw new Error('Invalid or expired activation token.');
}
const userId = payload.user_id;
// 3. Fetch user
const existing = await read.query(`
SELECT user_id, assigned_roles
FROM users
WHERE user_id = ?
`, [userId]);
if (!existing[0]) throw new Error('User not found.');
const user = existing[0];
// 4. Add "activated" role if not present
const roles = user.assigned_roles ? JSON.parse(user.assigned_roles) : [];
if (!roles.includes('activated')) roles.push('activated');
// 5. Update DB
await write.query(`
UPDATE users
SET assigned_roles = ?, last_active = NOW()
WHERE user_id = ?
`, [JSON.stringify(roles), userId]);
return { message: 'Account successfully activated.', assignedRoles: roles };
},
// check auth function
check: async (data) => {},
};
@@ -0,0 +1,68 @@
// ROOT/services/logger/readLogs.js
// Import the MySQL pools from your database service
const pools = require('../database/pools').get();
// Database name used for logging
const dbName = 'logging';
// Use the Read pool with the "system" role
const readPool = pools[dbName].r.s;
/*
* Function: getAllLogs
* Description: Retrieves all log entries from a specified table.
* @param {string} tableName - Name of the table to query.
* @returns {Array|Object} - Returns an array of log entries, or an error object.
*/
async function getAllLogs(tableName) {
if (!tableName) throw new Error('Table name is required');
try {
const [rows] = await readPool.query(`SELECT * FROM ${tableName}`);
return rows;
} catch (err) {
console.error('Error reading logs from DB:', err);
return { success: false, error: err };
}
}
/*
* Function: getLogs
* Description: Retrieves log entries from a specified table that match optional filter criteria.
* @param {string} tableName - Name of the table to query.
* @param {Object} filter - Optional key/value pairs representing column filters.
* Example: { user: 'andi', success: true }
* @returns {Array|Object} - Returns an array of filtered log entries, or an error object.
*/
async function getLogs(tableName, filter = {}) {
if (!tableName) throw new Error('Table name is required');
// Start building the SQL query
let sql = `SELECT * FROM ${tableName}`;
const values = [];
const filterKeys = Object.keys(filter);
if (filterKeys.length > 0) {
// Build WHERE clauses dynamically
const whereClauses = filterKeys.map(key => {
values.push(filter[key]);
return `${key} = ?`;
});
sql += ' WHERE ' + whereClauses.join(' AND ');
}
try {
const [rows] = await readPool.query(sql, values);
return rows;
} catch (err) {
console.error('Error reading filtered logs from DB:', err);
return { success: false, error: err };
}
}
// Export both functions for external use
module.exports = {
getAll: getAllLogs,
get: getLogs
};
@@ -0,0 +1,18 @@
let online = false;
module.exports = async function() {
try {
online = true;
console.log('Logger service started!');
return { online: online };
} catch (err) {
console.error('Logger service could not be started:', err);
return { online: false, error: err };
}
};
// Export online status for other modules
module.exports.online = () => online;
// Setter to change online status from other modules
module.exports.setOnline = (value) => { online = value; };
@@ -0,0 +1,5 @@
const logger = require('./start');
module.exports = async function() {
return { online: logger.online() };
};
@@ -0,0 +1,8 @@
const logger = require('./start');
module.exports = async function() {
// Set logger offline via setter in start.js
logger.setOnline(false);
console.log('Logger service stopped!');
return { online: logger.online() };
};
@@ -0,0 +1,56 @@
const loggerService = require('./start');
// Import the MySQL pools from your database service
const pools = require('../database/pools').get();
// Name of the database used for logging
const dbName = 'logging';
// Get the Write pool with the "system" role
const writePool = pools[dbName].w.s;
/*
* Generic function to write log data to any table
* @param {string} tableName - Name of the table to write to
* @param {Object} logData - Key/value pairs representing columns and values
* @returns {Object} - Success status and optional error
*/
async function writeToTable(tableName, logData) {
// Check if logger is active
if (!loggerService.online()) {
return { success: false, error: 'Logger is inactive' };
}
// Validate input
if (!tableName || !logData) throw new Error('Table name and log data are required');
// Extract columns and values dynamically from the logData object
const columns = Object.keys(logData).join(', '); // e.g., "user, action, success"
const placeholders = Object.keys(logData).map(() => '?').join(', '); // e.g., "?, ?, ?"
const values = Object.values(logData); // e.g., ["user", "login", true]
// Build the INSERT SQL statement
const sql = `INSERT INTO ${tableName} (${columns}) VALUES (${placeholders})`;
try {
// Execute the query on the write pool
await writePool.query(sql, values);
// Return success
return { success: true };
} catch (err) {
// Log the error to the console for debugging
console.error('Error writing log to DB:', err);
// Return failure state
return { success: false, error: err };
}
}
/*
* Generic write function for any other log types.
* This allows dynamic handling of new log types without modifying the module.
*/
module.exports.write = async function(logData, tableName) {
return writeToTable(tableName, logData);
};
@@ -0,0 +1,25 @@
// variables
const services = { start: {}, stop: {}, restart: {}, status: {} };
const serviceNames = ['mysql','websocket','logger','scheduler'];
// dynamic manager
['start', 'stop', 'status'].forEach(action => {
serviceNames.forEach(name => {
services[action][name] = async () => {
const file = `./${name}/${action}.js`;
const func = require(file);
return await func();
};
});
});
// restart function
serviceNames.forEach(name => {
services.restart[name] = async () => {
await services.stop[name]();
await services.start[name]();
};
});
// function export
module.exports = services;
@@ -0,0 +1,191 @@
// node dependencies
const fs = require('fs');
const path = require('path');
// Path to the tasks.json file, which stores all tasks persistently
const tasksFile = path.join(__dirname, 'tasks.json');
module.exports = {
// In-memory object holding all tasks
/* Each task has the following structure:
tasks[taskName] = {
active: false, // whether the task is currently running
timer: null, // reference to setTimeout for execution
nextRun: null, // timestamp of next scheduled execution
lastRun: null, // timestamp of last execution
error: null, // last error if the task failed
oneTime: true/false, // if true, task runs only once
intervalInSec: 0 // interval in seconds for repeated execution
action: string // name of the JS file in ROOT/tasks
}
*/
tasks: {},
// Load tasks from tasks.json into memory
loadTasks: function() {
// Create empty file if missing
if (!fs.existsSync(tasksFile)) {
fs.writeFileSync(tasksFile, '{}', 'utf-8');
}
// Read and parse JSON
const rawData = fs.readFileSync(tasksFile, 'utf-8');
const data = JSON.parse(rawData);
// Load each task into memory
for (const name in data) {
const taskData = data[name];
// Ensure timer is reset in memory
taskData.timer = null;
this.tasks[name] = taskData;
// If task is active, automatically start the timer
if (taskData.active) {
this._startTimer(name);
}
}
},
// Save current in-memory tasks to tasks.json - Removes timer references before saving
saveTasks: function() {
const dataToSave = {};
for (const name in this.tasks) {
const task = { ...this.tasks[name] };
delete task.timer; // timers cannot be persisted
dataToSave[name] = task;
}
fs.writeFileSync(tasksFile, JSON.stringify(dataToSave, null, 4), 'utf-8');
},
// Add or update a task in memory and save it to JSON
// If the task does not exist, initialize default fields
setTask: function(name, data) {
// If task does not exist, create default structure
if (!this.tasks[name]) {
this.tasks[name] = {
active: false,
timer: null,
nextRun: null,
lastRun: null,
error: null,
oneTime: false,
intervalInSec: null,
action: null
};
}
// Merge new data
Object.assign(this.tasks[name], data);
// Stop any running timer before updating
if (this.tasks[name].timer) {
clearTimeout(this.tasks[name].timer);
this.tasks[name].timer = null;
}
// Immediately start the timer if task is active
if (this.tasks[name].active) {
this._startTimer(name);
}
// Persist to JSON
this.saveTasks();
},
// Remove a task completely from memory and JSON
removeTask: function(name) {
const task = this.tasks[name];
// Stop running timer if it exists
if (task && task.timer) clearTimeout(task.timer);
// Remove from memory
delete this.tasks[name];
// Persist changes
this.saveTasks();
},
// Retrieve a single task by name
getTask: function(name) {
return this.tasks[name] || null;
},
// Retrieve all tasks in memory
getAllTasks: function() {
return this.tasks;
},
/*
* Internal function to start the timer for a task
* - Loads the action from ROOT/tasks
* - Executes the function at intervals or once depending on task.oneTime
* @param {string} name - Task name
*/
_startTimer: function(name) {
const task = this.tasks[name];
// Task must exist and have an action defined
if (!task || !task.action) return;
// Build path to task action file
const taskPath = path.join(__dirname, '../../../tasks', task.action + '.js');
let actionFunc;
try {
// Load the exported function from the task file
actionFunc = require(taskPath);
if (typeof actionFunc !== 'function') {
throw new Error('Task action must export a function');
}
} catch (err) {
console.error(`Failed to load action for task "${name}":`, err.message);
task.error = err.message;
this.saveTasks();
return;
}
// Convert interval to milliseconds
const intervalMs = task.intervalInSec ? task.intervalInSec * 1000 : 0;
// Function executed by the timer
const executeTask = async () => {
task.lastRun = Date.now();
try {
// Call the action function
await actionFunc();
task.error = null;
} catch (err) {
task.error = err.message || String(err);
}
if (!task.oneTime && intervalMs && task.active) {
// Recurring task: schedule next run
task.nextRun = Date.now() + intervalMs;
task.timer = setTimeout(executeTask, intervalMs);
} else if (task.oneTime) {
// One-time task: deactivate after execution
task.active = false;
task.nextRun = null;
task.timer = null;
}
// Save updated task info
this.saveTasks();
};
// Activate the task and schedule first execution
task.active = true;
task.nextRun = Date.now() + intervalMs;
task.timer = setTimeout(executeTask, intervalMs);
// Save memory state
this.saveTasks();
}
};
@@ -0,0 +1,36 @@
const memory = require('./memory.js'); // Scheduler memory
module.exports = async function() {
try {
// Load tasks from tasks.json into memory
// Active tasks will automatically start their timers
memory.loadTasks();
// Build the return object with task status
const tasksStatus = {};
const allTasks = memory.getAllTasks();
for (const name in allTasks) {
const t = allTasks[name];
tasksStatus[name] = {
active: t.active,
oneTime: t.oneTime,
intervalInSec: t.intervalInSec,
lastRun: t.lastRun,
nextRun: t.nextRun,
error: t.error,
action: t.action
};
}
return {
online: true, // service is running
tasks: tasksStatus
};
} catch (err) {
console.error('Failed to start scheduler service:', err);
return {
online: false,
tasks: {}
};
}
};
@@ -0,0 +1,36 @@
const memory = require('./memory.js'); // Scheduler memory
module.exports = async function() {
try {
const allTasks = memory.getAllTasks();
// Build the return object with task status
const tasksStatus = {};
for (const name in allTasks) {
const t = allTasks[name];
tasksStatus[name] = {
active: t.active, // whether the task is currently marked active
oneTime: t.oneTime, // one-time or recurring
intervalInSec: t.intervalInSec,
lastRun: t.lastRun, // last execution timestamp
nextRun: t.nextRun, // next scheduled execution timestamp
error: t.error, // last error if occurred
action: t.action // linked action file
};
}
// Determine if service is online (any task has a timer running)
const online = Object.values(allTasks).some(t => t.timer);
return {
online: online,
tasks: tasksStatus
};
} catch (err) {
console.error('Failed to retrieve scheduler status:', err);
return {
online: false,
tasks: {}
};
}
};
@@ -0,0 +1,42 @@
const memory = require('./memory.js'); // Scheduler memory
module.exports = async function() {
try {
const allTasks = memory.getAllTasks();
// Stop all running timers without changing 'active' state
for (const name in allTasks) {
const task = allTasks[name];
if (task.timer) {
clearTimeout(task.timer);
task.timer = null;
}
}
// Build the return object with task status
const tasksStatus = {};
for (const name in allTasks) {
const t = allTasks[name];
tasksStatus[name] = {
active: t.active,
oneTime: t.oneTime,
intervalInSec: t.intervalInSec,
lastRun: t.lastRun,
nextRun: t.nextRun,
error: t.error,
action: t.action
};
}
return {
online: false, // service is stopped
tasks: tasksStatus
};
} catch (err) {
console.error('Failed to stop scheduler service:', err);
return {
online: false,
tasks: {}
};
}
};
@@ -0,0 +1 @@
{}
@@ -0,0 +1,86 @@
// node dependencies
const WebSocket = require('ws');
const fs = require('fs');
const path = require('path');
require('dotenv').config({ path: '../../.env' });
// internal dependencies
const connections = require('./connections.js'); // connection manager
const stopService = require('./stop.js');
module.exports = async function startWebsocketServer() {
loadActions(actionsDir);
try {
const wss = new WebSocket.Server({
host: process.env.HOST,
port: parseInt(process.env.WS_PORT)
});
stopService.setServer(wss);
console.log('WebSocket server started on host localhost and port 3001');
// Start heartbeat system
connections.startHeartbeat();
wss.on('connection', (ws, req) => {
if (key !== process.env.INTERNAL_SECRET || url.origin !== process.env.CONN_URI) {
ws.close();
return;
}
// Add client to connection manager
const connectionId = connections.add(ws, {
connectedAt: Date.now()
});
console.log('New client connected:', connectionId);
// Handle incoming messages
ws.on('message', async (raw) => {
let msg;
try {
msg = JSON.parse(raw);
} catch (err) {
console.error('Invalid JSON received:', raw);
return;
}
// Expected format:
// { action: "namespace.actionName", data: {...} }
if (!msg.action) return;
const actionHandler = actions[msg.action];
if (!actionHandler) {
console.warn('Unknown action:', msg.action);
return;
}
try {
await actionHandler(msg.data, msg.sec, ws, connectionId);
} catch (err) {
console.error('Error executing action:', msg.action, err);
}
});
// Remove connection on close
ws.on('close', () => {
console.log('Client disconnected:', connectionId);
connections.remove(connectionId);
});
// Remove connection on error
ws.on('error', (err) => {
console.error('WebSocket error on', connectionId, err);
connections.remove(connectionId);
});
});
return { online: true, server: wss };
} catch (err) {
console.error('WebSocket server failed to start:', err);
return { online: false, error: err };
}
};
@@ -0,0 +1,36 @@
// internal dependencies
const connections = require('./connections.js'); // connection manager
const stopService = require('./stop.js'); // for server reference
/*
* Get WebSocket service status
* Returns:
* {
* online: boolean,
* clients: {
* connectionId1: { metadata: {...}, lastHeartbeat: ... },
* connectionId2: { ... }
* }
* }
*/
module.exports = async function websocketStatus() {
try {
// check if server exists and is running
const serverOnline = !!stopService.getServer();
// get active clients
const clients = connections.getAll(); // object with connectionId -> { metadata, lastHeartbeat }
return {
online: serverOnline,
clients
};
} catch (err) {
console.error('Error getting WebSocket status:', err);
return {
online: false,
clients: {},
error: err
};
}
};
@@ -0,0 +1,42 @@
// internal dependencies
const connections = require('./connections.js');
// This will hold a reference to the running WebSocket server
let serverInstance = null;
/*
* Stop the WebSocket service
* - Stops heartbeat
* - Closes all active connections
* - Closes the WebSocket server
*/
async function stopService () {
try {
if (!serverInstance) return { online: false };
// Stop heartbeat system
connections.stopHeartbeat();
// Terminate all active connections
connections.clearAll();
// Close WebSocket server
await new Promise((resolve, reject) => {
serverInstance.close((err) => {
if (err) return reject(err);
resolve();
});
});
serverInstance = null;
console.log('WebSocket server stopped successfully');
return { online: false };
} catch (err) {
console.error('Error stopping WebSocket server:', err);
return { online: true, error: err };
}
};
module.exports = stopService;
+12
View File
@@ -0,0 +1,12 @@
/*
! /services/idGenerator.js
= Central Unique ID-Generator
? This module will create unique ids using crypto
*/
// % import nodejs dependencies
import crypto from 'crypto';
// $ create a new unique id
export function generate() { return crypto.randomBytes(32).toString('hex'); } // simple id generator
export function generatePrefixed(prefix) { return `${prefix}_${generate()}`; } // prefixed id generator