CentralBackend/services/matrix/MatrixService.js
2025-11-17 22:11:28 +01:00

276 lines
7.0 KiB
JavaScript

const MatrixRepository = require('../../repositories/matrix/MatrixRepository');
const Matrix = require('../../models/Matrix');
function isAdmin(user) {
return !!user && ['admin', 'super_admin'].includes(user.role);
}
function isValidEmail(s) {
return typeof s === 'string' && /\S+@\S+\.\S+/.test(s);
}
function toBool(value, defaultVal = false) {
if (typeof value === 'boolean') return value;
if (typeof value === 'string') {
const v = value.trim().toLowerCase();
if (v === 'true') return true;
if (v === 'false') return false;
}
return defaultVal;
}
async function create({ name, topNodeEmail, force = false, actorUser }) {
if (!actorUser) {
const err = new Error('Unauthorized');
err.status = 401;
throw err;
}
if (!isAdmin(actorUser)) {
const err = new Error('Forbidden: Admins only');
err.status = 403;
throw err;
}
const trimmedName = (name || '').trim();
if (!trimmedName) {
const err = new Error('Matrix name is required');
err.status = 400;
throw err;
}
if (trimmedName.length > 255) {
const err = new Error('Matrix name is too long');
err.status = 400;
throw err;
}
const email = (topNodeEmail || '').trim().toLowerCase();
if (!isValidEmail(email)) {
const err = new Error('Valid top node email is required');
err.status = 400;
throw err;
}
const res = await MatrixRepository.createMatrix({ name: trimmedName, topNodeEmail: email, force });
return new Matrix({
name: res.name,
masterTopUserId: res.masterTopUserId,
masterTopUserEmail: res.masterTopUserEmail,
// also expose root user id for frontend
rootUserId: res.masterTopUserId
});
}
async function getStats({ actorUser }) {
if (!actorUser) {
const err = new Error('Unauthorized');
err.status = 401;
throw err;
}
if (!isAdmin(actorUser)) {
const err = new Error('Forbidden: Admins only');
err.status = 403;
throw err;
}
const stats = await MatrixRepository.getMatrixStats();
// Keep the response shape straightforward for the dashboard
return {
activeMatrices: stats.activeMatrices,
totalMatrices: stats.totalMatrices,
totalUsersSubscribed: stats.totalUsersSubscribed,
matrices: stats.matrices.map(m => {
const rootId = Number(m.rootUserId);
const matrixConfigId = m.matrixConfigId != null ? Number(m.matrixConfigId) : null;
const matrixId = matrixConfigId != null ? matrixConfigId : rootId; // prefer config id when available
return {
// primary fields
rootUserId: rootId,
name: m.name,
isActive: !!m.isActive,
usersCount: m.usersCount,
createdAt: m.createdAt, // equals ego_activated_at
topNodeEmail: m.topNodeEmail,
// compatibility and routing aliases for frontend
root_user_id: rootId,
matrixConfigId,
matrixId,
id: matrixId
};
})
};
}
async function getUsers({ rootUserId, maxDepth, limit, offset, includeRoot, actorUser, matrixId, topNodeEmail }) {
if (!actorUser) {
const err = new Error('Unauthorized');
err.status = 401;
throw err;
}
if (!isAdmin(actorUser)) {
const err = new Error('Forbidden: Admins only');
err.status = 403;
throw err;
}
// resolve root id from any of the accepted identifiers
let rid = Number.parseInt(rootUserId, 10);
if (!Number.isFinite(rid) || rid <= 0) {
rid = await MatrixRepository.resolveRootUserId({ rootUserId, matrixId, topNodeEmail });
}
// NEW: do not clamp to 5 globally; repository will enforce per-root policy
let depth = Number.parseInt(maxDepth ?? 5, 10);
if (!Number.isFinite(depth)) depth = 5;
if (depth < 0) depth = 0;
let lim = Number.parseInt(limit ?? 100, 10);
if (!Number.isFinite(lim) || lim <= 0) lim = 100;
if (lim > 500) lim = 500;
let off = Number.parseInt(offset ?? 0, 10);
if (!Number.isFinite(off) || off < 0) off = 0;
const incRoot = toBool(includeRoot, false);
const users = await MatrixRepository.getMatrixUsers({
rootUserId: rid,
maxDepth: depth,
limit: lim,
offset: off,
includeRoot: incRoot
});
return {
rootUserId: rid,
maxDepth: depth,
limit: lim,
offset: off,
includeRoot: incRoot,
users
};
}
// NEW: search user candidates to add into a matrix
async function getUserCandidates({ q, type, limit, offset, rootUserId, matrixId, topNodeEmail, actorUser }) {
if (!actorUser) {
const err = new Error('Unauthorized');
err.status = 401;
throw err;
}
if (!isAdmin(actorUser)) {
const err = new Error('Forbidden: Admins only');
err.status = 403;
throw err;
}
const query = (q || '').trim();
const normType = ['personal', 'company', 'all'].includes(String(type).toLowerCase())
? String(type).toLowerCase()
: 'all';
let lim = Number.parseInt(limit ?? 20, 10);
if (!Number.isFinite(lim) || lim <= 0) lim = 20;
if (lim > 50) lim = 50;
let off = Number.parseInt(offset ?? 0, 10);
if (!Number.isFinite(off) || off < 0) off = 0;
// Return empty list when query too short
if (query.length < 2) {
return {
q: query,
type: normType,
rootUserId: null,
limit: lim,
offset: off,
total: 0,
items: []
};
}
// Resolve root id if not a valid positive number
let rid = Number.parseInt(rootUserId, 10);
if (!Number.isFinite(rid) || rid <= 0) {
rid = await MatrixRepository.resolveRootUserId({ rootUserId, matrixId, topNodeEmail });
}
const { total, items } = await MatrixRepository.getUserSearchCandidates({
q: query,
type: normType,
rootUserId: rid,
limit: lim,
offset: off
});
return {
q: query,
type: normType,
rootUserId: rid,
limit: lim,
offset: off,
total,
items
};
}
async function addUserToMatrix({
rootUserId,
matrixId,
topNodeEmail,
childUserId,
forceParentFallback,
parentUserId,
actorUser
}) {
if (!actorUser) {
const err = new Error('Unauthorized');
err.status = 401;
throw err;
}
if (!isAdmin(actorUser)) {
const err = new Error('Forbidden: Admins only');
err.status = 403;
throw err;
}
const cId = Number(childUserId);
if (!Number.isFinite(cId) || cId <= 0) {
const err = new Error('Invalid childUserId');
err.status = 400;
throw err;
}
const fallback = toBool(forceParentFallback, false);
const parentOverride = Number(parentUserId) > 0 ? Number(parentUserId) : undefined;
const summary = await MatrixRepository.addUserToMatrix({
rootUserId,
matrixId,
topNodeEmail,
childUserId: cId,
forceParentFallback: fallback,
parentUserId: parentOverride,
actorUserId: actorUser.id
});
// fetch a small updated list (depth 2, limit 25)
const users = await MatrixRepository.getMatrixUsers({
rootUserId: summary.rootUserId,
maxDepth: 2,
limit: 25,
offset: 0,
includeRoot: true
});
return {
...summary,
usersPreview: users
};
}
module.exports = {
create,
getStats,
getUsers,
getUserCandidates,
addUserToMatrix // NEW
};