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