18 KiB
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.
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
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
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
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
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:
{
action: "",
data: { ... }
}
- action: string that matches a loaded action file (ROOT/websocket)
- data: payload passed to the action handler
Example:
{
"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:
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:
// 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
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
{
"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
asyncto ensure proper execution of asynchronous operations.
Usage
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
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
onlineflag 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
onlineflag is set tofalse, preventing any writes to the database. - Log entries are written dynamically to tables in the
loggingdatabase via thewrite()function. - The MySQL write pool (
w.s,writemethod →systemrole) 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.
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), thewrite()function returns{ success: false, error: 'Logger is inactive' }. - Columns in
logDatamust 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
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
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_ROUNDSin.env). - Sessions are managed with JWTs and expire according to
JWT_EXPIRES. - User pictures are resized to
250x250pxPNGs 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.