Files
safetynet/apps/server/public/group-admin.js
T

17 lines
8.0 KiB
JavaScript
Raw Normal View History

2026-10-13 10:33:31 +02:00
/* Group restrictions are additive. Preview and acknowledgment describe actual backend state. */
let groupState={groups:[],devices:[],version:0},selectedGroup='',groupBookmarks=[];
const groupFlags=[['group-disable-downloads','disableDownloads'],['group-disable-ai','disableAI'],['group-disable-agents','disableAgents'],['group-hide-settings','hideSettings'],['group-require-scan','requireDocumentScan']];
function groupChecks(container,devices,selected){$(container).replaceChildren();for(const device of devices){const label=document.createElement('label'),input=document.createElement('input');label.className='checks';input.type='checkbox';input.value=device.id;input.checked=selected.includes(device.id);input.disabled=device.revoked;label.append(input,document.createTextNode(`${device.name}${device.revoked?' (revoked)':''}`));$(container).append(label);}}
function groupSelected(container){return Array.from($(container).querySelectorAll('input:checked'),input=>input.value);}
function groupDraft(){const stage=$('group-stage').value,restrictions={blockedHosts:lines('group-blocked-hosts'),blockedPhrases:lines('group-blocked-phrases'),hiddenSections:managedSections.filter(section=>$(`group-hide-${section}`).checked),lockedSections:managedSections.filter(section=>$(`group-lock-${section}`).checked),managedBookmarks:structuredClone(groupBookmarks)};for(const [id,key]of groupFlags)restrictions[key]=$(id).checked;return {id:$('group-id').value.trim(),name:$('group-name').value.trim(),memberDeviceIds:groupSelected('group-members'),restrictions,rollout:{stage,deviceIds:stage==='all'?[]:groupSelected('group-cohort')}};}
function editGroup(group){selectedGroup=group?.id||'';groupBookmarks=structuredClone(group?.restrictions.managedBookmarks||[]);renderBookmarkRows('group-bookmark-rows',groupBookmarks);$('group-editor-title').textContent=group?`Edit ${group.name}`:'Create a device group';$('group-id').value=group?.id||'';$('group-id').readOnly=!!group;$('group-name').value=group?.name||'';$('group-stage').value=group?.rollout.stage||'pilot';groupChecks('group-members',groupState.devices,group?.memberDeviceIds||[]);groupChecks('group-cohort',groupState.devices,group?.rollout.deviceIds||[]);$('group-blocked-hosts').value=(group?.restrictions.blockedHosts||[]).join('\n');$('group-blocked-phrases').value=(group?.restrictions.blockedPhrases||[]).join('\n');for(const [id,key]of groupFlags)$(id).checked=group?.restrictions[key]===true;for(const section of managedSections){$(`group-hide-${section}`).checked=group?.restrictions.hiddenSections?.includes(section)||false;$(`group-lock-${section}`).checked=group?.restrictions.lockedSections?.includes(section)||false;}$('group-preview-summary').textContent='';$('group-preview-diff').replaceChildren();updateGroupCohort();applyGroupPermissions(currentPrincipal);}
function updateGroupCohort(){const members=groupSelected('group-members'),all=$('group-stage').value==='all';for(const checkbox of $('group-cohort').querySelectorAll('input')){if(!members.includes(checkbox.value))checkbox.checked=false;checkbox.disabled=all||!members.includes(checkbox.value);} }
function applyGroupPermissions(principal){if(!principal)return;const permissions=new Set(principal.permissions||[]),write=permissions.has('policy.write')||(independentApprovalRequired&&permissions.has('policy.prepare'));for(const input of $('policy-groups').querySelectorAll('input,textarea,select'))input.disabled=!write;for(const id of ['groups-refresh','group-preview-device','group-preview'])$(id).disabled=!permissions.has('policy.read');for(const id of ['group-save','group-new','group-bookmark-add'])$(id).disabled=!write;$('group-delete').disabled=!write||!selectedGroup;for(const button of $('group-bookmark-rows').querySelectorAll('button'))button.disabled=!write;for(const button of $('groups-rows').querySelectorAll('button'))button.disabled=!permissions.has('policy.read');$('group-save').textContent=independentApprovalRequired?'Prepare group for independent approval':'Publish group and rollout';if(write)updateGroupCohort();}
async function loadPolicyGroups(){groupState=await api('/api/admin/policy-groups');$('groups-rows').replaceChildren();for(const group of groupState.groups){const row=document.createElement('tr');cell(row,group.name);cell(row,group.memberDeviceIds.length);cell(row,group.rollout.stage==='all'?'All members':`${group.rollout.stage} · ${group.rollout.deviceIds.length} devices`);const action=cell(row,''),button=document.createElement('button');button.className='secondary';button.textContent='Edit / expand rollout';button.onclick=()=>editGroup(group);action.append(button);$('groups-rows').append(row);}$('group-device-rows').replaceChildren();$('group-preview-device').replaceChildren();for(const device of groupState.devices){const row=document.createElement('tr');cell(row,device.name);cell(row,(device.groups||[]).join(', ')||'Baseline');cell(row,`${device.policyVersion??0} / ${device.targetVersion??groupState.version}`);cell(row,device.revoked?'Revoked':device.acknowledged?'Reported target version':'Awaiting target version');cell(row,device.last_seen?new Date(device.last_seen).toLocaleString():'No contact');$('group-device-rows').append(row);if(!device.revoked){const option=document.createElement('option');option.value=device.id;option.textContent=device.name;$('group-preview-device').append(option);}}$('groups-status').textContent=`${groupState.groups.length} groups · signed baseline v${groupState.version}`;editGroup(groupState.groups.find(group=>group.id===selectedGroup));}
$('group-sections').replaceChildren();for(const section of managedSections){const row=document.createElement('div');row.className='checks';for(const [kind,label]of [['hide','Hide'],['lock','Lock']]){const element=document.createElement('label'),input=document.createElement('input');input.type='checkbox';input.id=`group-${kind}-${section}`;element.append(input,document.createTextNode(`${label} ${section}`));row.append(element);}$('group-sections').append(row);}
$('groups-refresh').onclick=task(loadPolicyGroups);$('group-new').onclick=()=>editGroup();$('group-members').onchange=updateGroupCohort;$('group-stage').onchange=updateGroupCohort;
$('group-save').onclick=task(async()=>{const group=groupDraft();if(!group.id||!group.name)throw Error('Enter a group identifier and name');const result=await api(`/api/admin/policy-groups/${encodeURIComponent(group.id)}`,{expectedVersion:groupState.version,group},'PUT');selectedGroup=group.id;await load();policyMutationStatus(result,`Group and ${group.rollout.stage} rollout published as signed policy v${result.version}.`);});
$('group-delete').onclick=task(async()=>{if(!selectedGroup)throw Error('Select an existing group');const result=await api(`/api/admin/policy-groups/${encodeURIComponent(selectedGroup)}`,{expectedVersion:groupState.version},'DELETE');selectedGroup='';await load();policyMutationStatus(result,`Group removed in signed policy v${result.version}.`);});
$('group-preview').onclick=task(async()=>{const group=groupDraft(),body={deviceId:$('group-preview-device').value};if(group.id)body.group=group;const result=await api('/api/admin/policy-groups/preview',body);$('group-preview-summary').textContent=`Effective groups: ${(result.groups||[]).join(', ')||'baseline only'} · ${result.diff?.changes?.length||0} differences from baseline`;$('group-preview-diff').replaceChildren();for(const change of result.diff?.changes||[]){const item=document.createElement('article');item.className='change-item';const title=document.createElement('strong'),before=document.createElement('pre'),after=document.createElement('pre');title.textContent=change.path;before.textContent=`Baseline: ${JSON.stringify(change.before)}`;after.textContent=`Effective: ${JSON.stringify(change.after)}`;item.append(title,before,after);$('group-preview-diff').append(item);}if(result.diff?.truncated){const warning=document.createElement('p');warning.textContent='Preview differences truncated; inspect the complete effective policy before publishing.';$('group-preview-diff').append(warning);}});
$('group-bookmark-add').onclick=task(async()=>addBookmark(groupBookmarks,'group-bookmark-rows'));