Compare commits
15 Commits
release-pki
...
fix-68
| Author | SHA1 | Date | |
|---|---|---|---|
| a6c24850d4 | |||
| 7da5050ce3 | |||
| 1cb693a1eb | |||
| 5c3a8cefe1 | |||
| 15b3a424bc | |||
| 6cb309b6d9 | |||
| f0ceb750a8 | |||
| 6e95defcf5 | |||
| 92c2e8a03b | |||
| 230e5be2fd | |||
| 0331cb976a | |||
| 90cf65e920 | |||
| 2d202b4979 | |||
| 8f04c20cd7 | |||
| df330c6c0f |
Binary file not shown.
@@ -58,6 +58,7 @@ class PluginInstance extends Model {
|
|||||||
lastRunAt: { type: 'integer' },
|
lastRunAt: { type: 'integer' },
|
||||||
lastStatus: { type: 'string' },
|
lastStatus: { type: 'string' },
|
||||||
lastError: { type: 'text' },
|
lastError: { type: 'text' },
|
||||||
|
lastLog: { type: 'text' },
|
||||||
// Audit stamps (set by the route handler, not by an ORM hook).
|
// Audit stamps (set by the route handler, not by an ORM hook).
|
||||||
created_by: { type: 'string' },
|
created_by: { type: 'string' },
|
||||||
created_on: { type: 'integer' },
|
created_on: { type: 'integer' },
|
||||||
|
|||||||
@@ -96,11 +96,13 @@ class Resource extends Model {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let maxUpdated = 0;
|
||||||
resObjs.forEach(r => {
|
resObjs.forEach(r => {
|
||||||
r.metadata.isProduction = checkProd(r.id);
|
r.metadata.isProduction = checkProd(r.id);
|
||||||
|
if (r.updated_on && r.updated_on > maxUpdated) maxUpdated = r.updated_on;
|
||||||
});
|
});
|
||||||
|
|
||||||
return { resources: resObjs, edges };
|
return { resources: resObjs, edges, updated_on: maxUpdated || Date.now() };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stamp `resolvedAddress` on each resource: its own address/ip if it has one,
|
// Stamp `resolvedAddress` on each resource: its own address/ip if it has one,
|
||||||
|
|||||||
@@ -773,7 +773,7 @@ User.setActive = async function(active) {
|
|||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
await client.modify(this.dn, [
|
await client.modify(this.dn, [
|
||||||
new Change({ operation: 'replace', modification: new Attribute({ type: 'pwdAccountLockedTime', values: ['000001010000Z'] }) }),
|
new Change({ operation: 'replace', modification: new Attribute({ type: 'pwdAccountLockedTime', values: ['00000101000000Z'] }) }),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -788,7 +788,7 @@ User.setActive = async function(active) {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.pwdAccountLockedTime = active ? undefined : '000001010000Z';
|
this.pwdAccountLockedTime = active ? undefined : '00000101000000Z';
|
||||||
this.isActive = active ? 'active' : '';
|
this.isActive = active ? 'active' : '';
|
||||||
this.isInactive = active ? '' : 'inactive';
|
this.isInactive = active ? '' : 'inactive';
|
||||||
cache.clear();
|
cache.clear();
|
||||||
@@ -907,6 +907,13 @@ User.login = async function(data){
|
|||||||
}
|
}
|
||||||
let user = await this.get(data.uid || data.username);
|
let user = await this.get(data.uid || data.username);
|
||||||
|
|
||||||
|
if (user.pwdAccountLockedTime) {
|
||||||
|
let error = new Error('Invalid Credentials, login failed.');
|
||||||
|
error.name = 'LDAPLoginFailed';
|
||||||
|
error.status = 401;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
const loginClient = makeClient();
|
const loginClient = makeClient();
|
||||||
try {
|
try {
|
||||||
await loginClient.bind(user.dn, data.password);
|
await loginClient.bind(user.dn, data.password);
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "t42-sso-manager",
|
"name": "t42-sso-manager",
|
||||||
"version": "1.19.0",
|
"version": "1.19.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "t42-sso-manager",
|
"name": "t42-sso-manager",
|
||||||
"version": "1.19.0",
|
"version": "1.19.4",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^7.3.0",
|
"@fortawesome/fontawesome-free": "^7.3.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "t42-sso-manager",
|
"name": "t42-sso-manager",
|
||||||
"version": "1.19.0",
|
"version": "1.19.4",
|
||||||
"description": "A very simple LDAP management and SSO system",
|
"description": "A very simple LDAP management and SSO system",
|
||||||
"author": [
|
"author": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -31,17 +31,25 @@ module.exports = {
|
|||||||
if (!targetRange) throw new Error("Missing targetRange for Nmap");
|
if (!targetRange) throw new Error("Missing targetRange for Nmap");
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const scan = new nmap.OsAndPortScan(targetRange);
|
// OsAndPortScan requires root (for -O). NmapScan does a basic port scan (TCP connect if non-root).
|
||||||
|
const scan = new nmap.NmapScan(targetRange);
|
||||||
scan.command.push('-Pn');
|
scan.command.push('-Pn');
|
||||||
|
scan.command.push('-F'); // fast scan, 100 top ports
|
||||||
|
scan.command.push('--min-rate', '100'); // speed up the scan
|
||||||
|
|
||||||
|
if (config.log) config.log(`Starting nmap scan: ${scan.command.join(' ')}`);
|
||||||
|
|
||||||
scan.on('complete', function(data) {
|
scan.on('complete', function(data) {
|
||||||
|
if (config.log) config.log(`Scan complete. Found ${data ? data.length : 0} hosts.`);
|
||||||
const resources = [];
|
const resources = [];
|
||||||
const edges = [];
|
const edges = [];
|
||||||
|
|
||||||
for (const host of data) {
|
for (const host of data) {
|
||||||
if (!host.mac || !host.ip) continue;
|
if (!host.ip) continue;
|
||||||
const hostSlug = `nmap-host-${host.mac.replace(/:/g, '')}`;
|
const hostId = host.mac ? host.mac.replace(/:/g, '') : host.ip.replace(/\\./g, '_');
|
||||||
|
const hostSlug = `nmap-host-${hostId}`;
|
||||||
|
|
||||||
const interfaces = [{ mac: host.mac, ip: host.ip }];
|
const interfaces = [{ mac: host.mac || null, ip: host.ip }];
|
||||||
|
|
||||||
resources.push({
|
resources.push({
|
||||||
kind: 'host',
|
kind: 'host',
|
||||||
@@ -52,7 +60,7 @@ module.exports = {
|
|||||||
|
|
||||||
if (host.openPorts && host.openPorts.length > 0) {
|
if (host.openPorts && host.openPorts.length > 0) {
|
||||||
for (const port of host.openPorts) {
|
for (const port of host.openPorts) {
|
||||||
const svcSlug = `nmap-svc-${host.mac.replace(/:/g, '')}-${port.port}`;
|
const svcSlug = `nmap-svc-${hostId}-${port.port}`;
|
||||||
resources.push({
|
resources.push({
|
||||||
kind: 'service',
|
kind: 'service',
|
||||||
name: `${port.service} on ${port.port}`,
|
name: `${port.service} on ${port.port}`,
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
discover: async (config) => {
|
discover: async (config) => {
|
||||||
const { url, tokenId, tokenSecret } = config;
|
let { url, tokenId, tokenSecret } = config;
|
||||||
if (!url || !tokenId || !tokenSecret) {
|
if (!url || !tokenId || !tokenSecret) {
|
||||||
throw new Error("Missing Proxmox config");
|
throw new Error("Missing Proxmox config");
|
||||||
}
|
}
|
||||||
@@ -44,6 +44,9 @@ module.exports = {
|
|||||||
'Authorization': `PVEAPIToken=${tokenId}=${tokenSecret}`
|
'Authorization': `PVEAPIToken=${tokenId}=${tokenSecret}`
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Ensure URL has no trailing slash
|
||||||
|
url = url.endsWith('/') ? url.slice(0, -1) : url;
|
||||||
|
|
||||||
const resources = [];
|
const resources = [];
|
||||||
const edges = [];
|
const edges = [];
|
||||||
|
|
||||||
|
|||||||
@@ -201,6 +201,16 @@ router.post('/resources/:id/rotate-secret', async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.post('/resources/:id/service-token', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { ServiceToken } = require('../models/token');
|
||||||
|
const token = await ServiceToken.issue(req.params.id, req.user.uid);
|
||||||
|
res.json({ results: { token: token.token } });
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
router.delete('/resources/:id', async (req, res, next) => {
|
router.delete('/resources/:id', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const r = await Resource.get(req.params.id);
|
const r = await Resource.get(req.params.id);
|
||||||
|
|||||||
@@ -274,7 +274,7 @@ router.get('/:id/runs', async (req, res, next) => {
|
|||||||
try {
|
try {
|
||||||
const inst = await PluginInstance.get(req.params.id);
|
const inst = await PluginInstance.get(req.params.id);
|
||||||
if (!inst) return res.status(404).json({ error: 'Not found' });
|
if (!inst) return res.status(404).json({ error: 'Not found' });
|
||||||
res.json({ results: { lastRunAt: inst.lastRunAt, lastStatus: inst.lastStatus, lastError: inst.lastError } });
|
res.json({ results: { lastRunAt: inst.lastRunAt, lastStatus: inst.lastStatus, lastError: inst.lastError, lastLog: inst.lastLog } });
|
||||||
} catch (err) { next(err); }
|
} catch (err) { next(err); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ router.get('/graph', async (req, res, next) => {
|
|||||||
res.json(envelope({
|
res.json(envelope({
|
||||||
resources: projectResources(graph.resources, { fullMetadata }),
|
resources: projectResources(graph.resources, { fullMetadata }),
|
||||||
edges: graph.edges,
|
edges: graph.edges,
|
||||||
|
updated_on: graph.updated_on
|
||||||
}));
|
}));
|
||||||
} catch (err) { next(err); }
|
} catch (err) { next(err); }
|
||||||
});
|
});
|
||||||
@@ -98,6 +99,42 @@ router.get('/me', async (req, res, next) => {
|
|||||||
} catch (err) { next(err); }
|
} catch (err) { next(err); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// GET /api/discovery/access/:uid[/:slug]
|
||||||
|
// Answers per-user access for a machine caller (e.g. jump-host).
|
||||||
|
router.get(['/access/:uid', '/access/:uid/:slug'], async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { fullMetadata } = await callerView(req);
|
||||||
|
if (!req.user || (!req.user.isMachine && !fullMetadata)) {
|
||||||
|
return res.status(403).json(envelope({ error: 'Only machine identities or admins may query access for other users.' }));
|
||||||
|
}
|
||||||
|
const { User } = require('../models/user_ldap');
|
||||||
|
const { groupCns } = require('../utils/user_groups');
|
||||||
|
|
||||||
|
const targetUser = await User.get(req.params.uid).catch(() => null);
|
||||||
|
if (!targetUser) return res.status(404).json(envelope({ error: 'User not found' }));
|
||||||
|
|
||||||
|
const groups = await groupCns(targetUser);
|
||||||
|
const ids = new Set();
|
||||||
|
if (groups.length) {
|
||||||
|
const rgs = await ResourceGroup.list({ where: { groupCn: { in: groups } } });
|
||||||
|
for (const rg of rgs) ids.add(rg.resourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
let all = await Resource.list();
|
||||||
|
if (req.params.slug) all = all.filter(r => r.slug === req.params.slug);
|
||||||
|
|
||||||
|
let accessible = all.filter(r => {
|
||||||
|
const isAuto = r.metadata?.discovery_sources?.length > 0 && !r.metadata.discovery_sources.includes('manual');
|
||||||
|
const isManaged = r.metadata?.managed === true;
|
||||||
|
if (isAuto && !isManaged) return false;
|
||||||
|
return ids.has(r.id) || (r.metadata && r.metadata.isPublic);
|
||||||
|
});
|
||||||
|
|
||||||
|
accessible = await Resource.withResolvedAddress(accessible);
|
||||||
|
res.json(envelope(projectResources(accessible, { fullMetadata })));
|
||||||
|
} catch (err) { next(err); }
|
||||||
|
});
|
||||||
|
|
||||||
// POST /api/discovery/sync
|
// POST /api/discovery/sync
|
||||||
// Used by external agents (e.g. ldap-client) to push discovery data.
|
// Used by external agents (e.g. ldap-client) to push discovery data.
|
||||||
router.post('/sync', async (req, res, next) => {
|
router.post('/sync', async (req, res, next) => {
|
||||||
|
|||||||
@@ -90,7 +90,8 @@ router.get('/plugins', function(req, res, next) {
|
|||||||
// 'app_sso_directory_admin','admin']) and the /api/plugins endpoints enforce
|
// 'app_sso_directory_admin','admin']) and the /api/plugins endpoints enforce
|
||||||
// the same server-side. Same header-vs-navigation auth model as /conf and
|
// the same server-side. Same header-vs-navigation auth model as /conf and
|
||||||
// /vault (auth-token is a client-set header, not a cookie).
|
// /vault (auth-token is a client-set header, not a cookie).
|
||||||
res.render('plugins', {...values});
|
const registry = require('../services/plugin_registry');
|
||||||
|
res.render('plugins', {...values, pluginTypes: registry.types });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/vault', function(req, res) {
|
router.get('/vault', function(req, res) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class DiscoveryReconciler {
|
|||||||
|
|
||||||
for (const res of resources) {
|
for (const res of resources) {
|
||||||
if (!res.metadata) res.metadata = {};
|
if (!res.metadata) res.metadata = {};
|
||||||
|
res._originalSlug = res.slug; // Keep track for edge mapping
|
||||||
|
|
||||||
let existing = null;
|
let existing = null;
|
||||||
|
|
||||||
@@ -94,10 +95,11 @@ class DiscoveryReconciler {
|
|||||||
metadata: mergedMeta,
|
metadata: mergedMeta,
|
||||||
updated_on: Math.floor(Date.now() / 1000)
|
updated_on: Math.floor(Date.now() / 1000)
|
||||||
});
|
});
|
||||||
|
res._actualId = existing.id;
|
||||||
} else {
|
} else {
|
||||||
// Create new
|
// Create new
|
||||||
const sources = [sourceName];
|
const sources = new Set([sourceName]);
|
||||||
res.metadata.discovery_sources = sources;
|
res.metadata.discovery_sources = [...sources];
|
||||||
res.metadata.last_seen = Date.now();
|
res.metadata.last_seen = Date.now();
|
||||||
|
|
||||||
const slug = res.slug || `${res.kind}-${crypto.randomBytes(4).toString('hex')}`;
|
const slug = res.slug || `${res.kind}-${crypto.randomBytes(4).toString('hex')}`;
|
||||||
@@ -112,12 +114,48 @@ class DiscoveryReconciler {
|
|||||||
});
|
});
|
||||||
|
|
||||||
newDevices++;
|
newDevices++;
|
||||||
|
res._actualId = created.id; // Map original slug to actual ID
|
||||||
WebhookEmitter.emit('discovery.new_device', created.toJSON());
|
WebhookEmitter.emit('discovery.new_device', created.toJSON());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We can handle edges similarly if needed, but for simplicity we assume edges are managed elsewhere
|
// Now process edges
|
||||||
// or we just trust the plugins to give us explicit parent-child mappings by slug.
|
const allRes = await Resource.list();
|
||||||
|
const existingEdges = await ResourceEdge.list();
|
||||||
|
|
||||||
|
for (const edge of edges) {
|
||||||
|
// Find parent ID. It might be in the current payload (mapped to _actualId) or in DB by slug
|
||||||
|
let parentId = null;
|
||||||
|
const parentResInPayload = resources.find(r => r._originalSlug === edge.parentSlug);
|
||||||
|
if (parentResInPayload && parentResInPayload._actualId) {
|
||||||
|
parentId = parentResInPayload._actualId;
|
||||||
|
} else {
|
||||||
|
const parentResInDb = allRes.find(r => r.slug === edge.parentSlug);
|
||||||
|
if (parentResInDb) parentId = parentResInDb.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find child ID
|
||||||
|
let childId = null;
|
||||||
|
const childResInPayload = resources.find(r => r._originalSlug === edge.childSlug);
|
||||||
|
if (childResInPayload && childResInPayload._actualId) {
|
||||||
|
childId = childResInPayload._actualId;
|
||||||
|
} else {
|
||||||
|
const childResInDb = allRes.find(r => r.slug === edge.childSlug);
|
||||||
|
if (childResInDb) childId = childResInDb.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parentId && childId) {
|
||||||
|
const edgeExists = existingEdges.find(e => e.parentId === parentId && e.childId === childId && e.relation === edge.relation);
|
||||||
|
if (!edgeExists) {
|
||||||
|
await ResourceEdge.create({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
parentId,
|
||||||
|
childId,
|
||||||
|
relation: edge.relation
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (newDevices > 0) {
|
if (newDevices > 0) {
|
||||||
console.log(`[DiscoveryReconciler] Source ${sourceName} discovered ${newDevices} new devices.`);
|
console.log(`[DiscoveryReconciler] Source ${sourceName} discovered ${newDevices} new devices.`);
|
||||||
|
|||||||
@@ -71,17 +71,23 @@ async function runPluginJob(instanceId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[Scheduler] Running plugin: ${instance.slug} (${instance.pluginType})`);
|
console.log(`[Scheduler] Running plugin: ${instance.slug} (${instance.pluginType})`);
|
||||||
await instance.update({ lastRunAt: Date.now(), lastStatus: STATUS.RUNNING, lastError: null });
|
await instance.update({ lastRunAt: Date.now(), lastStatus: STATUS.RUNNING, lastError: null, lastLog: null });
|
||||||
|
let logs = [];
|
||||||
try {
|
try {
|
||||||
const cfg = await pluginSecrets.mergeForRun(instance);
|
const cfg = await pluginSecrets.mergeForRun(instance);
|
||||||
|
cfg.log = (msg) => {
|
||||||
|
logs.push(`[${new Date().toISOString()}] ${msg}`);
|
||||||
|
console.log(`[Plugin ${instance.slug}] ${msg}`);
|
||||||
|
if (logs.length > 1000) logs.shift();
|
||||||
|
};
|
||||||
const payload = await runFn(cfg);
|
const payload = await runFn(cfg);
|
||||||
if (instance.category === 'discovery') {
|
if (instance.category === 'discovery') {
|
||||||
await DiscoveryReconciler.reconcile(instance.slug, payload);
|
await DiscoveryReconciler.reconcile(instance.slug, payload);
|
||||||
}
|
}
|
||||||
await instance.update({ lastStatus: STATUS.OK, lastError: null });
|
await instance.update({ lastStatus: STATUS.OK, lastError: null, lastLog: logs.join('\n') });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[Scheduler] Plugin ${instance.slug} failed:`, err.message);
|
console.error(`[Scheduler] Plugin ${instance.slug} failed:`, err.message);
|
||||||
await instance.update({ lastStatus: STATUS.ERROR, lastError: String(err.message || err) });
|
await instance.update({ lastStatus: STATUS.ERROR, lastError: String(err.message || err), lastLog: logs.join('\n') });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,11 +21,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-wrap gap-2 align-items-center">
|
<div class="d-flex flex-wrap gap-2 align-items-center">
|
||||||
<input type="text" id="search-filter" class="form-control form-control-sm shadow-sm" placeholder="Search resources..." onkeyup="renderTable()" style="width: 250px;">
|
<input type="text" id="search-filter" class="form-control form-control-sm shadow-sm" placeholder="Search resources..." onkeyup="renderTable()" style="width: 250px;">
|
||||||
<select id="filter-managed" class="form-select form-select-sm shadow-sm" onchange="renderTable()" style="width: 150px;">
|
|
||||||
<option value="all">All Resources</option>
|
|
||||||
<option value="unmanaged" selected>Unmanaged Only</option>
|
|
||||||
<option value="managed">Managed Only</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-header actionMessage" style="display:none"></div>
|
<div class="card-header actionMessage" style="display:none"></div>
|
||||||
@@ -132,10 +127,9 @@
|
|||||||
// Name search
|
// Name search
|
||||||
if(search && !r.name.toLowerCase().includes(search) && !r.slug.toLowerCase().includes(search)) return false;
|
if(search && !r.name.toLowerCase().includes(search) && !r.slug.toLowerCase().includes(search)) return false;
|
||||||
|
|
||||||
// Managed filter
|
// Always hide items that have been committed to the catalog (managed)
|
||||||
const isManaged = !!(r.metadata && r.metadata.managed);
|
const isManaged = !!(r.metadata && r.metadata.managed);
|
||||||
if(managedFilter === 'managed' && !isManaged) return false;
|
if(isManaged) return false;
|
||||||
if(managedFilter === 'unmanaged' && isManaged) return false;
|
|
||||||
|
|
||||||
const isAuto = r.metadata && r.metadata.discovery_sources && r.metadata.discovery_sources.length > 0 && !r.metadata.discovery_sources.includes('manual');
|
const isAuto = r.metadata && r.metadata.discovery_sources && r.metadata.discovery_sources.length > 0 && !r.metadata.discovery_sources.includes('manual');
|
||||||
if(!isAuto) return false;
|
if(!isAuto) return false;
|
||||||
@@ -164,7 +158,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$('.actionMessage').html('<div class="alert alert-success alert-dismissible"><button type="button" class="btn-close" data-bs-dismiss="alert"></button>Successfully promoted! Created groups: ' + res.groups.join(', ') + '</div>').show();
|
$('.actionMessage').html('<div class="alert alert-success alert-dismissible"><button type="button" class="btn-close" data-bs-dismiss="alert"></button>Successfully promoted! Created groups: ' + res.groups.join(', ') + '</div>').show();
|
||||||
renderTable();
|
loadResources();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,7 @@
|
|||||||
<button class="btn btn-sm btn-warning" title="Edit Secrets" onclick="openSecretsModal('{{id}}')"><i class="fa-solid fa-key"></i></button>
|
<button class="btn btn-sm btn-warning" title="Edit Secrets" onclick="openSecretsModal('{{id}}')"><i class="fa-solid fa-key"></i></button>
|
||||||
<button class="btn btn-sm btn-info" title="Test" onclick="testPlugin('{{id}}')"><i class="fa-solid fa-vial"></i></button>
|
<button class="btn btn-sm btn-info" title="Test" onclick="testPlugin('{{id}}')"><i class="fa-solid fa-vial"></i></button>
|
||||||
<button class="btn btn-sm btn-success" title="Run now" onclick="runNow('{{id}}')"><i class="fa-solid fa-play"></i></button>
|
<button class="btn btn-sm btn-success" title="Run now" onclick="runNow('{{id}}')"><i class="fa-solid fa-play"></i></button>
|
||||||
|
{{#lastRunAt}}<button class="btn btn-sm btn-secondary" title="View Logs" onclick="showLogs('{{id}}')"><i class="fa-solid fa-file-lines"></i></button>{{/lastRunAt}}
|
||||||
{{#enabled}}<button class="btn btn-sm btn-outline-danger" title="Unload" onclick="togglePlugin('{{id}}', false)">Unload</button>{{/enabled}}
|
{{#enabled}}<button class="btn btn-sm btn-outline-danger" title="Unload" onclick="togglePlugin('{{id}}', false)">Unload</button>{{/enabled}}
|
||||||
{{^enabled}}<button class="btn btn-sm btn-outline-success" title="Load" onclick="togglePlugin('{{id}}', true)">Load</button>{{/enabled}}
|
{{^enabled}}<button class="btn btn-sm btn-outline-success" title="Load" onclick="togglePlugin('{{id}}', true)">Load</button>{{/enabled}}
|
||||||
<button class="btn btn-sm btn-outline-danger" title="Delete" onclick="deletePlugin('{{id}}')"><i class="fa-solid fa-trash"></i></button>
|
<button class="btn btn-sm btn-outline-danger" title="Delete" onclick="deletePlugin('{{id}}')"><i class="fa-solid fa-trash"></i></button>
|
||||||
@@ -280,7 +281,7 @@
|
|||||||
'<div class="form-text">Secret fields are edited separately with the <i class="fa-solid fa-key"></i> button.</div>',
|
'<div class="form-text">Secret fields are edited separately with the <i class="fa-solid fa-key"></i> button.</div>',
|
||||||
footer: {
|
footer: {
|
||||||
metaHtml: app.modal.formatAudit ? app.modal.formatAudit(p, { formatDate: function(ms){ return moment(ms).format('YYYY-MM-DD HH:mm'); } }) : '',
|
metaHtml: app.modal.formatAudit ? app.modal.formatAudit(p, { formatDate: function(ms){ return moment(ms).format('YYYY-MM-DD HH:mm'); } }) : '',
|
||||||
buttonsHtml: app.modal.footerButtons({ onSave: 'saveEdit("' + id + '")', saveLabel: 'Save' })
|
buttonsHtml: app.modal.footerButtons({ onSave: 'saveEdit(\'' + id + '\')', saveLabel: 'Save' })
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -324,7 +325,7 @@
|
|||||||
app.modal.open({
|
app.modal.open({
|
||||||
title: 'Edit Secrets — ' + p.name,
|
title: 'Edit Secrets — ' + p.name,
|
||||||
bodyHtml: html,
|
bodyHtml: html,
|
||||||
footer: { buttonsHtml: app.modal.footerButtons({ onSave: 'saveSecrets("' + id + '")', saveLabel: 'Save Secrets' }) }
|
footer: { buttonsHtml: app.modal.footerButtons({ onSave: 'saveSecrets(\'' + id + '\')', saveLabel: 'Save Secrets' }) }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,6 +370,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function showLogs(id) {
|
||||||
|
var p = pluginsById[id];
|
||||||
|
if (!p) return;
|
||||||
|
try {
|
||||||
|
const res = await app.api.get('plugins/' + id + '/runs');
|
||||||
|
const logText = (res.results && res.results.lastLog) || (res.results && res.results.lastError) || 'No logs available.';
|
||||||
|
app.modal.open({
|
||||||
|
title: 'Logs — ' + p.name,
|
||||||
|
bodyHtml: '<pre class="bg-dark text-white p-3 rounded" style="white-space: pre-wrap; font-size: 0.85em;">' + String(logText).replace(/</g, '<').replace(/>/g, '>') + '</pre>',
|
||||||
|
footer: { buttonsHtml: '<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>' }
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
app.messages.toast('Failed to load logs: ' + (err.message || err), 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function togglePlugin(id, enable) {
|
async function togglePlugin(id, enable) {
|
||||||
try {
|
try {
|
||||||
await app.api.post('plugins/' + id + (enable ? '/load' : '/unload'), {});
|
await app.api.post('plugins/' + id + (enable ? '/load' : '/unload'), {});
|
||||||
|
|||||||
Reference in New Issue
Block a user