276 lines
7.0 KiB
JavaScript
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
|
|
};
|