thatcher 1.0.59 → 1.0.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -64,6 +64,16 @@ export class ConfigGeneratorEngine {
|
|
|
64
64
|
return this;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
updatePermissionTemplate(templateName, roleActionsMap) {
|
|
68
|
+
if (!templateName || typeof templateName !== 'string') {
|
|
69
|
+
throw new Error('[ConfigGeneratorEngine] updatePermissionTemplate: templateName required');
|
|
70
|
+
}
|
|
71
|
+
const nextTemplates = { ...(this.masterConfig.permission_templates || {}), [templateName]: roleActionsMap };
|
|
72
|
+
this.masterConfig = deepFreeze({ ...this.masterConfig, permission_templates: nextTemplates });
|
|
73
|
+
this.specCache.clear();
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
|
|
67
77
|
registerPlugin(entityName, plugin = {}) {
|
|
68
78
|
if (!entityName || typeof entityName !== 'string') {
|
|
69
79
|
throw new Error('[ConfigGeneratorEngine] registerPlugin: entityName required');
|
package/src/server/server.js
CHANGED
|
@@ -118,6 +118,10 @@ export function createServer(options) {
|
|
|
118
118
|
return await handleUpdateWorkflow(req, res, id, thatcher, configEngine);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
if (req.method === 'POST' && entity === 'permission-template' && id && action === 'update') {
|
|
122
|
+
return await handleUpdatePermissionTemplate(req, res, id, thatcher, configEngine);
|
|
123
|
+
}
|
|
124
|
+
|
|
121
125
|
// Check if user has custom route for this
|
|
122
126
|
const userRoutePath = path.join(process.cwd(), 'app/api', ...parts, 'route.js');
|
|
123
127
|
const routeExists = await fileExists(userRoutePath);
|
|
@@ -345,6 +349,80 @@ async function handleGenericCrud(req, res, entity, id, action, thatcher, configE
|
|
|
345
349
|
}
|
|
346
350
|
}
|
|
347
351
|
|
|
352
|
+
const PERMISSION_ACTIONS = new Set(['list', 'view', 'create', 'edit', 'delete', 'archive', 'export', 'manage_settings']);
|
|
353
|
+
|
|
354
|
+
async function handleUpdatePermissionTemplate(req, res, templateName, thatcher, configEngineArg) {
|
|
355
|
+
const user = await resolveRequestUser(req);
|
|
356
|
+
if (!user) {
|
|
357
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
358
|
+
res.end(JSON.stringify({ error: 'Authentication required' }));
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const { isPartner } = await import('../ui/permissions-ui.js');
|
|
362
|
+
if (!isPartner(user)) {
|
|
363
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
364
|
+
res.end(JSON.stringify({ error: 'Forbidden' }));
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
let configEngine = configEngineArg || thatcher?.configEngine || globalThis.__thatcherConfigEngine;
|
|
369
|
+
if (!configEngine) {
|
|
370
|
+
const { getConfigEngineSync } = await import('../lib/config-generator-engine.js');
|
|
371
|
+
configEngine = getConfigEngineSync();
|
|
372
|
+
}
|
|
373
|
+
const existingTemplate = configEngine.getConfig().permission_templates?.[templateName];
|
|
374
|
+
if (!existingTemplate) {
|
|
375
|
+
res.writeHead(404);
|
|
376
|
+
res.end(JSON.stringify({ error: `Permission template "${templateName}" not found` }));
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const validRoles = new Set(Object.keys(configEngine.getRoles()));
|
|
380
|
+
|
|
381
|
+
let body;
|
|
382
|
+
try {
|
|
383
|
+
body = await readBody(req);
|
|
384
|
+
} catch (e) {
|
|
385
|
+
res.writeHead(400);
|
|
386
|
+
res.end(JSON.stringify({ error: e.message }));
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const roles = body && typeof body.roles === 'object' && !Array.isArray(body.roles) ? body.roles : null;
|
|
390
|
+
if (!roles) {
|
|
391
|
+
res.writeHead(400);
|
|
392
|
+
res.end(JSON.stringify({ error: 'roles object required' }));
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
for (const [roleName, actions] of Object.entries(roles)) {
|
|
396
|
+
if (!validRoles.has(roleName)) {
|
|
397
|
+
res.writeHead(400);
|
|
398
|
+
res.end(JSON.stringify({ error: `unknown role "${roleName}"` }));
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (!Array.isArray(actions)) {
|
|
402
|
+
res.writeHead(400);
|
|
403
|
+
res.end(JSON.stringify({ error: `actions for role "${roleName}" must be an array` }));
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
for (const a of actions) {
|
|
407
|
+
if (!PERMISSION_ACTIONS.has(a)) {
|
|
408
|
+
res.writeHead(400);
|
|
409
|
+
res.end(JSON.stringify({ error: `unknown action "${a}" for role "${roleName}"` }));
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
try {
|
|
416
|
+
configEngine.updatePermissionTemplate(templateName, roles);
|
|
417
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
418
|
+
res.end(JSON.stringify({ ok: true, template: templateName, roleCount: Object.keys(roles).length }));
|
|
419
|
+
} catch (err) {
|
|
420
|
+
apiLog.error(err.message);
|
|
421
|
+
res.writeHead(500);
|
|
422
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
348
426
|
async function handleUpdateWorkflow(req, res, workflowName, thatcher, configEngineArg) {
|
|
349
427
|
const user = await resolveRequestUser(req);
|
|
350
428
|
if (!user) {
|
|
@@ -7,6 +7,7 @@ import { renderSettingsNotifications, renderSettingsIntegrations, renderSettings
|
|
|
7
7
|
import { renderChecklistsManagement } from '@/ui/checklist-renderer.js';
|
|
8
8
|
import { renderJobManagement } from '@/ui/job-management-renderer.js';
|
|
9
9
|
import { renderWorkflowList, renderWorkflowEditor } from '@/ui/workflow-builder-renderer.js';
|
|
10
|
+
import { renderRolesList, renderTemplateList, renderPermissionMatrix } from '@/ui/rbac-renderer.js';
|
|
10
11
|
import { isPartner, isManager } from '@/ui/permissions-ui.js';
|
|
11
12
|
import { getSystemConfig, getSettingsCounts, getAuditData, getSystemHealth, renderBuildLogsContent } from '@/ui/page-handler-helpers.js';
|
|
12
13
|
import { fileURLToPath } from 'url';
|
|
@@ -142,5 +143,25 @@ export async function handleAdminPage(normalized, segments, user) {
|
|
|
142
143
|
if (!workflowDef) return null;
|
|
143
144
|
return renderWorkflowEditor(user, segments[2], workflowDef);
|
|
144
145
|
}
|
|
146
|
+
if (normalized === '/admin/roles') {
|
|
147
|
+
const { getConfigEngineSync } = await import('@/lib/config-generator-engine.js');
|
|
148
|
+
const engine = getConfigEngineSync();
|
|
149
|
+
return renderRolesList(user, engine.getRoles());
|
|
150
|
+
}
|
|
151
|
+
if (normalized === '/admin/permissions') {
|
|
152
|
+
const { getConfigEngineSync } = await import('@/lib/config-generator-engine.js');
|
|
153
|
+
const engine = getConfigEngineSync();
|
|
154
|
+
const config = engine.getConfig();
|
|
155
|
+
return renderTemplateList(user, Object.keys(config.permission_templates || {}));
|
|
156
|
+
}
|
|
157
|
+
if (segments.length === 3 && segments[1] === 'permissions') {
|
|
158
|
+
const { getConfigEngineSync } = await import('@/lib/config-generator-engine.js');
|
|
159
|
+
const engine = getConfigEngineSync();
|
|
160
|
+
const config = engine.getConfig();
|
|
161
|
+
const roleActionsMap = config.permission_templates?.[segments[2]];
|
|
162
|
+
if (!roleActionsMap) return null;
|
|
163
|
+
const roleNames = Object.keys(engine.getRoles());
|
|
164
|
+
return renderPermissionMatrix(user, segments[2], roleNames, roleActionsMap);
|
|
165
|
+
}
|
|
145
166
|
return null;
|
|
146
167
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { page } from '@/ui/layout.js';
|
|
2
|
+
import { esc } from '@/ui/render-helpers.js';
|
|
3
|
+
|
|
4
|
+
const ACTIONS = ['list', 'view', 'create', 'edit', 'delete', 'archive', 'export', 'manage_settings'];
|
|
5
|
+
|
|
6
|
+
export function renderRolesList(user, roles) {
|
|
7
|
+
const sorted = Object.entries(roles).sort((a, b) => (a[1].hierarchy ?? 999) - (b[1].hierarchy ?? 999));
|
|
8
|
+
const rows = sorted.map(([name, def]) =>
|
|
9
|
+
`<tr data-row><td>${esc(name)}</td><td>${esc(def.label || name)}</td><td>${esc(String(def.hierarchy ?? '-'))}</td><td>${esc(def.permissions_scope || '-')}</td></tr>`
|
|
10
|
+
).join('') || '<tr><td colspan="4">No roles configured</td></tr>';
|
|
11
|
+
const content = `<div class="page-header"><h1 class="page-title">Roles</h1></div>
|
|
12
|
+
<div class="table-wrap"><table class="data-table"><thead><tr><th>Name</th><th>Label</th><th>Hierarchy</th><th>Scope</th></tr></thead><tbody>${rows}</tbody></table></div>
|
|
13
|
+
<div style="margin-top:16px"><a href="/admin/permissions" class="btn-ghost-clean">Edit Permission Templates</a></div>`;
|
|
14
|
+
return page(user, 'Roles | Thatcher', [{ href: '/admin/settings', label: 'Settings' }, { label: 'Roles' }], content);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function renderTemplateList(user, templateNames) {
|
|
18
|
+
const rows = templateNames.map(name =>
|
|
19
|
+
`<tr data-row data-navigate="/admin/permissions/${esc(name)}" style="cursor:pointer"><td>${esc(name)}</td></tr>`
|
|
20
|
+
).join('') || '<tr><td>No permission templates configured</td></tr>';
|
|
21
|
+
const content = `<div class="page-header"><h1 class="page-title">Permission Templates</h1></div>
|
|
22
|
+
<div class="table-wrap"><table class="data-table"><thead><tr><th>Name</th></tr></thead><tbody>${rows}</tbody></table></div>`;
|
|
23
|
+
return page(user, 'Permission Templates | Thatcher', [{ href: '/admin/settings', label: 'Settings' }, { href: '/admin/roles', label: 'Roles' }, { label: 'Permission Templates' }], content);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function renderPermissionMatrix(user, templateName, roleNames, roleActionsMap) {
|
|
27
|
+
const header = ACTIONS.map(a => `<th style="text-align:center">${esc(a)}</th>`).join('');
|
|
28
|
+
const rows = roleNames.map(role => {
|
|
29
|
+
const actions = roleActionsMap[role] || [];
|
|
30
|
+
const cells = ACTIONS.map(a =>
|
|
31
|
+
`<td style="text-align:center"><input type="checkbox" data-role="${esc(role)}" data-action="${esc(a)}"${actions.includes(a) ? ' checked' : ''}></td>`
|
|
32
|
+
).join('');
|
|
33
|
+
return `<tr><td>${esc(role)}</td>${cells}</tr>`;
|
|
34
|
+
}).join('');
|
|
35
|
+
|
|
36
|
+
const content = `<div class="page-header"><h1 class="page-title">Permissions: ${esc(templateName)}</h1></div>
|
|
37
|
+
<div class="table-wrap"><table class="data-table" id="perm-matrix"><thead><tr><th>Role</th>${header}</tr></thead><tbody>${rows}</tbody></table></div>
|
|
38
|
+
<div style="margin-top:16px">
|
|
39
|
+
<button type="button" class="btn-primary-clean" data-action="savePermissions" data-args='["${esc(templateName)}"]'>Save</button>
|
|
40
|
+
<span id="save-status" style="margin-left:12px;font-size:13px"></span>
|
|
41
|
+
</div>`;
|
|
42
|
+
|
|
43
|
+
const script = `(function(){
|
|
44
|
+
window.savePermissions=function(templateName){
|
|
45
|
+
var boxes=document.querySelectorAll('#perm-matrix input[type="checkbox"]');
|
|
46
|
+
var map={};
|
|
47
|
+
boxes.forEach(function(cb){
|
|
48
|
+
var role=cb.getAttribute('data-role');
|
|
49
|
+
var action=cb.getAttribute('data-action');
|
|
50
|
+
if(!map[role])map[role]=[];
|
|
51
|
+
if(cb.checked)map[role].push(action);
|
|
52
|
+
});
|
|
53
|
+
var status=document.getElementById('save-status');
|
|
54
|
+
status.textContent='Saving...';
|
|
55
|
+
fetch('/api/permission-template/'+templateName+'/update',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({roles:map})})
|
|
56
|
+
.then(function(r){return r.json().then(function(d){return {ok:r.ok,d:d}})})
|
|
57
|
+
.then(function(res){if(res.ok){status.textContent='Saved';status.style.color='var(--color-success,#22c55e)';setTimeout(function(){location.reload()},500)}else{status.textContent='Error: '+(res.d.error||'save failed');status.style.color='var(--color-danger,#ef4444)'}})
|
|
58
|
+
.catch(function(err){status.textContent='Error: '+err.message;status.style.color='var(--color-danger,#ef4444)'});
|
|
59
|
+
};
|
|
60
|
+
})();`;
|
|
61
|
+
|
|
62
|
+
return page(user, `Permissions: ${templateName} | Thatcher`, [{ href: '/admin/settings', label: 'Settings' }, { href: '/admin/roles', label: 'Roles' }, { href: '/admin/permissions', label: 'Permission Templates' }, { label: templateName }], content, [script]);
|
|
63
|
+
}
|