186 lines
6.9 KiB
JavaScript
186 lines
6.9 KiB
JavaScript
import { db, id, json, now, parseJson, visibleClause } from '../db.js';
|
|
import { hostGroupSchema } from '../forms/schemas.js';
|
|
import { camelHostGroup } from './mappers.js';
|
|
import { filterVisibleHostIds } from './hostModel.js';
|
|
|
|
function scopedGroupId(payload) {
|
|
return payload.visibility === 'group' ? payload.groupId || null : null;
|
|
}
|
|
|
|
function normalizeHostGroup(body) {
|
|
const parsed = hostGroupSchema.parse(body || {});
|
|
return {
|
|
...parsed,
|
|
groupId: scopedGroupId(parsed),
|
|
rules: parsed.mode === 'dynamic' ? parsed.rules : [],
|
|
hostIds: parsed.mode === 'manual' ? parsed.hostIds : []
|
|
};
|
|
}
|
|
|
|
export function listHostGroups(userId) {
|
|
return db.prepare(`
|
|
SELECT hg.*, g.name AS group_name,
|
|
COALESCE((SELECT COUNT(*) FROM host_group_members hgm WHERE hgm.host_group_id = hg.id), 0) AS member_count,
|
|
COALESCE((SELECT json_group_array(host_id) FROM host_group_members hgm WHERE hgm.host_group_id = hg.id), '[]') AS host_ids
|
|
FROM host_groups hg
|
|
LEFT JOIN groups g ON g.id = hg.group_id
|
|
WHERE ${visibleClause('hg')}
|
|
ORDER BY hg.name
|
|
`).all(userId, userId).map(camelHostGroup);
|
|
}
|
|
|
|
export function getVisibleHostGroup(groupId, userId) {
|
|
const row = db.prepare(`
|
|
SELECT hg.*, g.name AS group_name,
|
|
COALESCE((SELECT COUNT(*) FROM host_group_members hgm WHERE hgm.host_group_id = hg.id), 0) AS member_count,
|
|
COALESCE((SELECT json_group_array(host_id) FROM host_group_members hgm WHERE hgm.host_group_id = hg.id), '[]') AS host_ids
|
|
FROM host_groups hg
|
|
LEFT JOIN groups g ON g.id = hg.group_id
|
|
WHERE hg.id = ? AND ${visibleClause('hg')}
|
|
`).get(groupId, userId, userId);
|
|
return row ? camelHostGroup(row) : null;
|
|
}
|
|
|
|
export function createHostGroup(body, userId) {
|
|
const payload = normalizeHostGroup(body);
|
|
if (payload.mode === 'dynamic' && !payload.rules.length) throw new Error('Dynamic host groups require at least one rule.');
|
|
const groupId = id('hgrp');
|
|
db.prepare(`
|
|
INSERT INTO host_groups (
|
|
id, name, description, mode, match_mode, rules, visibility, owner_user_id,
|
|
group_id, created_by, created_at, updated_at
|
|
)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`).run(
|
|
groupId,
|
|
payload.name,
|
|
payload.description,
|
|
payload.mode,
|
|
payload.matchMode,
|
|
json(payload.rules),
|
|
payload.visibility,
|
|
userId,
|
|
payload.groupId,
|
|
userId,
|
|
now(),
|
|
now()
|
|
);
|
|
if (payload.mode === 'manual') replaceHostGroupMembers(groupId, filterVisibleHostIds(payload.hostIds, userId), 'manual');
|
|
else syncHostGroup(groupId);
|
|
return getVisibleHostGroup(groupId, userId);
|
|
}
|
|
|
|
export function updateHostGroup(groupId, body, userId) {
|
|
const existing = db.prepare(`SELECT * FROM host_groups WHERE id = ? AND ${visibleClause()}`).get(groupId, userId, userId);
|
|
if (!existing) return null;
|
|
const payload = normalizeHostGroup({
|
|
name: existing.name,
|
|
description: existing.description || '',
|
|
mode: existing.mode,
|
|
matchMode: existing.match_mode,
|
|
rules: parseJson(existing.rules, []),
|
|
visibility: existing.visibility,
|
|
groupId: existing.group_id,
|
|
...body
|
|
});
|
|
if (payload.mode === 'dynamic' && !payload.rules.length) throw new Error('Dynamic host groups require at least one rule.');
|
|
db.prepare(`
|
|
UPDATE host_groups
|
|
SET name = ?, description = ?, mode = ?, match_mode = ?, rules = ?,
|
|
visibility = ?, group_id = ?, updated_at = ?
|
|
WHERE id = ?
|
|
`).run(
|
|
payload.name,
|
|
payload.description,
|
|
payload.mode,
|
|
payload.matchMode,
|
|
json(payload.rules),
|
|
payload.visibility,
|
|
payload.groupId,
|
|
now(),
|
|
groupId
|
|
);
|
|
if (payload.mode === 'manual') replaceHostGroupMembers(groupId, filterVisibleHostIds(payload.hostIds, userId), 'manual');
|
|
else syncHostGroup(groupId);
|
|
return getVisibleHostGroup(groupId, userId);
|
|
}
|
|
|
|
export function deleteHostGroup(groupId, userId) {
|
|
db.prepare(`DELETE FROM host_groups WHERE id = ? AND ${visibleClause()}`).run(groupId, userId, userId);
|
|
}
|
|
|
|
export function filterVisibleHostGroupIds(groupIds = [], userId) {
|
|
if (!groupIds.length) return [];
|
|
const visible = new Set(
|
|
db.prepare(`SELECT id FROM host_groups WHERE ${visibleClause()}`).all(userId, userId).map((row) => row.id)
|
|
);
|
|
return groupIds.filter((groupId) => visible.has(groupId));
|
|
}
|
|
|
|
export function syncHostGroup(groupId) {
|
|
const group = db.prepare('SELECT * FROM host_groups WHERE id = ?').get(groupId);
|
|
if (!group || group.mode !== 'dynamic') return { groupId, matched: 0, skipped: true };
|
|
const ownerId = group.owner_user_id || group.created_by;
|
|
const hosts = ownerId
|
|
? db.prepare(`SELECT * FROM hosts WHERE ${visibleClause()} ORDER BY name`).all(ownerId, ownerId)
|
|
: db.prepare("SELECT * FROM hosts WHERE visibility = 'shared' ORDER BY name").all();
|
|
const rules = parseJson(group.rules, []);
|
|
const matched = hosts.filter((host) => hostMatchesRules(host, rules, group.match_mode)).map((host) => host.id);
|
|
replaceHostGroupMembers(groupId, matched, 'rule');
|
|
db.prepare('UPDATE host_groups SET last_synced_at = ?, updated_at = ? WHERE id = ?').run(now(), now(), groupId);
|
|
return { groupId, matched: matched.length, skipped: false };
|
|
}
|
|
|
|
export function syncDynamicHostGroups() {
|
|
const groups = db.prepare("SELECT id FROM host_groups WHERE mode = 'dynamic'").all();
|
|
const summary = { evaluated: 0, synced: 0, matched: 0, errors: 0 };
|
|
for (const group of groups) {
|
|
summary.evaluated += 1;
|
|
try {
|
|
const result = syncHostGroup(group.id);
|
|
if (!result.skipped) {
|
|
summary.synced += 1;
|
|
summary.matched += result.matched;
|
|
}
|
|
} catch {
|
|
summary.errors += 1;
|
|
}
|
|
}
|
|
return summary;
|
|
}
|
|
|
|
function replaceHostGroupMembers(groupId, hostIds, source) {
|
|
db.prepare('DELETE FROM host_group_members WHERE host_group_id = ?').run(groupId);
|
|
const stmt = db.prepare(`
|
|
INSERT OR IGNORE INTO host_group_members (host_group_id, host_id, source, created_at)
|
|
VALUES (?, ?, ?, ?)
|
|
`);
|
|
for (const hostId of [...new Set(hostIds)]) stmt.run(groupId, hostId, source, now());
|
|
}
|
|
|
|
export function hostMatchesRules(host, rules = [], matchMode = 'all') {
|
|
if (!rules.length) return false;
|
|
const results = rules.map((rule) => hostMatchesRule(host, rule));
|
|
return matchMode === 'any' ? results.some(Boolean) : results.every(Boolean);
|
|
}
|
|
|
|
function hostMatchesRule(host, rule) {
|
|
const values = hostRuleValues(host, rule.field);
|
|
const needle = String(rule.value || '').toLowerCase();
|
|
return values.some((value) => compareRuleValue(String(value || '').toLowerCase(), needle, rule.operator));
|
|
}
|
|
|
|
function hostRuleValues(host, field) {
|
|
if (field === 'tags') return parseJson(host.tags, []);
|
|
if (field === 'sourceType') return [host.source_type || 'manual'];
|
|
if (field === 'osFamily') return [host.os_family || ''];
|
|
return [host[field] || ''];
|
|
}
|
|
|
|
function compareRuleValue(value, needle, operator = 'contains') {
|
|
if (operator === 'equals') return value === needle;
|
|
if (operator === 'startsWith') return value.startsWith(needle);
|
|
if (operator === 'endsWith') return value.endsWith(needle);
|
|
return value.includes(needle);
|
|
}
|