61 lines
4.4 KiB
JavaScript
61 lines
4.4 KiB
JavaScript
import {z} from 'zod';
|
|
import {PolicySchema} from '../../packages/core/policy.mjs';
|
|
import {globalBookmarksValid} from './bookmarks.mjs';
|
|
|
|
const version=z.coerce.number().int().positive().max(Number.MAX_SAFE_INTEGER);
|
|
function snapshot(store,id){
|
|
const row=store.db.prepare('SELECT version,time,actor,policy FROM policies WHERE version=?').get(id);
|
|
if(!row)return null;
|
|
const policy=PolicySchema.parse(JSON.parse(row.policy));
|
|
// Global history never carries device personal collections, even from old stores.
|
|
policy.browser.bookmarks.personal={revision:0,items:[]};
|
|
const groupsRow=store.db.prepare('SELECT groups FROM policy_group_history WHERE version=?').get(id);
|
|
const profilesRow=store.db.prepare('SELECT configuration FROM user_profile_history WHERE version=?').get(id);
|
|
return {...row,policy,userProfiles:profilesRow?JSON.parse(profilesRow.configuration):null,groups:groupsRow?JSON.parse(groupsRow.groups):[],groupHistoryAvailable:!!groupsRow};
|
|
}
|
|
export function policyDiff(before,after,max=500){
|
|
const changes=[];let truncated=false;
|
|
function walk(a,b,path){
|
|
if(JSON.stringify(a)===JSON.stringify(b))return;
|
|
if(a&&b&&typeof a==='object'&&typeof b==='object'&&!Array.isArray(a)&&!Array.isArray(b)){
|
|
for(const key of [...new Set([...Object.keys(a),...Object.keys(b)])].sort())walk(a[key],b[key],`${path}/${key.replaceAll('~','~0').replaceAll('/','~1')}`);
|
|
return;
|
|
}
|
|
if(changes.length>=max){truncated=true;return;}
|
|
changes.push({path,kind:a===undefined?'added':b===undefined?'removed':'changed',...(a===undefined?{}:{before:a}),...(b===undefined?{}:{after:b})});
|
|
}
|
|
walk(before,after,'');return {changes,truncated};
|
|
}
|
|
export function installPolicyHistory(app,store,publisher){
|
|
app.get('/api/admin/policy/history',(req,res)=>{
|
|
const query=z.object({before:version.optional(),limit:z.coerce.number().int().min(1).max(100).default(20)}).strict().parse(req.query);
|
|
const currentVersion=store.getRaw('policy').version;
|
|
const rows=store.db.prepare('SELECT version,time,actor FROM policies WHERE version<? ORDER BY version DESC LIMIT ?').all(query.before??Number.MAX_SAFE_INTEGER,query.limit+1);
|
|
const more=rows.length>query.limit,versions=rows.slice(0,query.limit).map(row=>({...row,current:row.version===currentVersion}));
|
|
res.json({currentVersion,versions,nextBefore:more?versions.at(-1).version:null});
|
|
});
|
|
app.get('/api/admin/policy/history/:version',(req,res)=>{
|
|
const item=snapshot(store,version.parse(req.params.version));
|
|
if(!item)return res.status(404).json({error:'Policy version not found'});
|
|
res.json(item);
|
|
});
|
|
app.get('/api/admin/policy/diff',(req,res)=>{
|
|
const query=z.object({from:version,to:version}).strict().parse(req.query);
|
|
const from=snapshot(store,query.from),to=snapshot(store,query.to);
|
|
if(!from||!to)return res.status(404).json({error:'Policy version not found'});
|
|
res.json({...query,...policyDiff(from.policy,to.policy),groupDiff:policyDiff(from.groups,to.groups),profileDiff:policyDiff(from.userProfiles,to.userProfiles)});
|
|
});
|
|
app.post('/api/admin/policy/restore',(req,res)=>{
|
|
const body=z.object({sourceVersion:version,expectedVersion:version,approve:z.literal(true)}).strict().parse(req.body);
|
|
const current=store.getRaw('policy');
|
|
if(current.version!==body.expectedVersion)return res.status(409).json({error:'Policy version conflict'});
|
|
const source=snapshot(store,body.sourceVersion);
|
|
if(!source)return res.status(404).json({error:'Policy version not found'});
|
|
const policy=PolicySchema.parse({...source.policy,version:current.version+1,expiresAt:new Date(Date.now()+7*86400000).toISOString()});
|
|
if(!globalBookmarksValid(store,policy))return res.status(409).json({error:'Historical company bookmark IDs conflict with current personal bookmarks'});
|
|
if(!source.groupHistoryAvailable&&(store.getRaw('policyGroups')||[]).length)return res.status(409).json({error:'Historical group assignment snapshot unavailable; review group changes explicitly'});
|
|
if(!source.userProfiles&&(store.getRaw('userProfiles')?.profiles?.length||store.getRaw('userProfiles')?.categories?.length))return res.status(409).json({error:'Historical user profile snapshot unavailable'});
|
|
publisher.submit(req,res,policy,'policy.restored',{type:'group-restore',sourceVersion:body.sourceVersion,groups:source.groups,configuration:source.userProfiles||{profiles:[],categories:[]}},{sourceVersion:body.sourceVersion,expiresAt:policy.expiresAt});
|
|
});
|
|
}
|