Initial commit
@@ -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"
|
||||
@@ -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 };
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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();
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
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"
|
||||
}
|
||||
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 228 KiB |
|
After Width: | Height: | Size: 291 KiB |
|
After Width: | Height: | Size: 264 KiB |
|
After Width: | Height: | Size: 981 KiB |
@@ -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;
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
@@ -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) => {
|
||||
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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.' };
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
|
||||