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 };