Files
safetynet/apps/server/user-profiles.mjs
T
2026-10-13 10:34:48 +02:00

29 lines
4.5 KiB
JavaScript

import {z} from 'zod';
import {UserProfileSchema,UserProfileConfigurationSchema,DomainCategorySchema,profileApplies,applyUserProfiles} from '../../packages/core/user-profiles.mjs';
import {PolicySchema} from '../../packages/core/policy.mjs';
import {applyPolicyGroups} from '../../packages/core/policy-groups.mjs';
import {policyDiff} from './policy-history.mjs';
export function profilesForOperation(store,operation={}){
const c=UserProfileConfigurationSchema.parse(store.getRaw('userProfiles')||{});
if(operation.type==='profile-restore'||operation.configuration)return UserProfileConfigurationSchema.parse(operation.configuration);
if(operation.type==='profile-upsert'){const p=UserProfileSchema.parse(operation.profile);c.profiles=c.profiles.filter(x=>x.id!==p.id);c.profiles.push(p);}
if(operation.type==='profile-delete')c.profiles=c.profiles.filter(x=>x.id!==operation.profileId);
if(operation.type==='profile-configuration')c.categories=z.array(DomainCategorySchema).max(100).parse(operation.categories);
return c;
}
export function validateProfiles(store,policy,operation={},groups=store.getRaw('policyGroups')||[]){
const c=profilesForOperation(store,operation),fail=m=>{throw Object.assign(Error(m),{status:409});};
if(new Set(c.profiles.map(p=>p.id)).size!==c.profiles.length||new Set(c.categories.map(p=>p.id)).size!==c.categories.length)fail('Duplicate profile/category ID');
for(const p of c.profiles){for(const id of p.blockedCategoryIds)if(!c.categories.some(x=>x.id===id))fail('Unknown domain category');for(const id of p.deviceIds)if(!store.db.prepare('SELECT id FROM devices WHERE id=? AND revoked=0').get(id))fail('Unknown active profile device');}
for(const subject of new Set(c.profiles.flatMap(p=>p.subjects)))PolicySchema.parse(applyUserProfiles(policy,c,{id:'future-enrollment',verified_subject:subject}));
for(const d of store.db.prepare('SELECT * FROM devices WHERE revoked=0').all())PolicySchema.parse(applyPolicyGroups(applyUserProfiles(policy,c,d),groups,d.id));
return c;
}
export function installUserProfiles(app,store,publisher){
app.get('/api/admin/user-profiles',(_req,res)=>{const c=profilesForOperation(store),p=store.get('policy');res.json({version:p.version,...c,delegation:{devTools:p.userAccess.delegatableFeatures.devTools},devices:store.db.prepare('SELECT id,name,user,verified_subject,revoked FROM devices').all().map(d=>({id:d.id,name:d.name,revoked:!!d.revoked,subject:d.verified_subject||null,verifiedSubject:!!d.verified_subject,profiles:c.profiles.filter(p=>profileApplies(p,d)).map(p=>p.id)}))});});
app.post('/api/admin/user-profiles/preview',(req,res)=>{const b=z.object({deviceId:z.string().min(1).max(80),profile:UserProfileSchema.optional()}).strict().parse(req.body),d=store.db.prepare('SELECT * FROM devices WHERE id=? AND revoked=0').get(b.deviceId);if(!d)return res.status(404).json({error:'Active device not found'});const p=store.get('policy'),c=profilesForOperation(store,b.profile?{type:'profile-upsert',profile:b.profile}:{}),policy=PolicySchema.parse(applyPolicyGroups(applyUserProfiles(p,c,d),store.getRaw('policyGroups')||[],d.id));res.json({policy,profiles:policy.assignment.profiles||[],diff:policyDiff(p,policy)});});
app.put('/api/admin/user-profile-configuration',(req,res)=>{const b=z.object({expectedVersion:z.number().int().positive(),categories:z.array(DomainCategorySchema).max(100),delegation:z.object({devTools:z.boolean()}).strict()}).strict().parse(req.body),p=store.get('policy');if(p.version!==b.expectedVersion)return res.status(409).json({error:'Policy version conflict'});p.version++;p.userAccess.delegatableFeatures.devTools=b.delegation.devTools;publisher.submit(req,res,p,'policy.profile-configuration',{type:'profile-configuration',categories:b.categories});});
app.put('/api/admin/user-profiles/:id',(req,res)=>{const b=z.object({expectedVersion:z.number().int().positive(),profile:UserProfileSchema}).strict().parse(req.body),p=store.get('policy');if(b.profile.id!==req.params.id)return res.status(400).json({error:'Profile ID mismatch'});if(p.version!==b.expectedVersion)return res.status(409).json({error:'Policy version conflict'});p.version++;publisher.submit(req,res,p,'policy.profile-published',{type:'profile-upsert',profile:b.profile});});
app.delete('/api/admin/user-profiles/:id',(req,res)=>{const b=z.object({expectedVersion:z.number().int().positive()}).strict().parse(req.body),p=store.get('policy');if(p.version!==b.expectedVersion)return res.status(409).json({error:'Policy version conflict'});p.version++;publisher.submit(req,res,p,'policy.profile-deleted',{type:'profile-delete',profileId:req.params.id});});
}