Compare commits

..

1 Commits

Author SHA1 Message Date
wmantly f9fb80c3b2 docs: update agents.md and index.md for Theta Agent C2 & Protocol v1.1.0
Pull Request Tests / Run Tests (18.x) (push) Failing after 1m28s
Pull Request Tests / Run Tests (20.x) (push) Failing after 28s
Pull Request Tests / Run Tests (22.x) (push) Failing after 28s
Pull Request Tests / Test Summary (push) Failing after 3s
2026-08-03 02:26:37 -04:00
10 changed files with 272 additions and 438 deletions
Binary file not shown.
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "t42-sso-manager", "name": "t42-sso-manager",
"version": "1.20.1", "version": "1.19.6",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "t42-sso-manager", "name": "t42-sso-manager",
"version": "1.20.0", "version": "1.19.6",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^7.3.0", "@fortawesome/fontawesome-free": "^7.3.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "t42-sso-manager", "name": "t42-sso-manager",
"version": "1.20.1", "version": "1.19.6",
"description": "A very simple LDAP management and SSO system", "description": "A very simple LDAP management and SSO system",
"author": [ "author": [
{ {
+1 -1
View File
@@ -186,7 +186,7 @@ router.post('/promote/:slug', async (req, res, next) => {
const meta = resource.metadata || {}; const meta = resource.metadata || {};
meta.managed = true; meta.managed = true;
await Resource.update(resource.id, { metadata: meta }); await resource.update({ metadata: meta });
res.json(envelope({ success: true, groups: [accessGroup, adminGroup] })); res.json(envelope({ success: true, groups: [accessGroup, adminGroup] }));
} catch (err) { next(err); } } catch (err) { next(err); }
+8 -1
View File
@@ -84,7 +84,14 @@ router.get('/discovery', function(req, res, next) {
}); });
router.get('/plugins', function(req, res, next) { router.get('/plugins', function(req, res, next) {
res.redirect('/directory'); // Plugin instances page — loadable/unloadable, configurable plugin copies
// with per-instance secrets in OpenBao. Renders the shell for anyone; the
// client gates with app.auth.forceLogin(['app_sso_admin',
// 'app_sso_directory_admin','admin']) and the /api/plugins endpoints enforce
// the same server-side. Same header-vs-navigation auth model as /conf and
// /vault (auth-token is a client-set header, not a cookie).
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) {
+13 -23
View File
@@ -12,39 +12,32 @@ class DiscoveryReconciler {
res._originalSlug = res.slug; // Keep track for edge mapping res._originalSlug = res.slug; // Keep track for edge mapping
let existing = null; let existing = null;
const normalizeMac = (m) => (m || '').toLowerCase().replace(/[^a-f0-9]/g, '');
const normalizeHost = (h) => (h || '').toLowerCase().split('.')[0].trim();
const allRes = await Resource.list(); // Attempt matching by MAC if available (case-insensitive)
// 1. Attempt matching by MAC (highest precision)
if (res.metadata.interfaces && res.metadata.interfaces.length > 0) { if (res.metadata.interfaces && res.metadata.interfaces.length > 0) {
const macs = res.metadata.interfaces.map(i => normalizeMac(i.mac)).filter(m => m.length === 12); const macs = res.metadata.interfaces.map(i => i.mac ? i.mac.toLowerCase() : null).filter(m => !!m);
if (macs.length > 0) { if (macs.length > 0) {
const allRes = await Resource.list();
existing = allRes.find(r => existing = allRes.find(r =>
r.metadata && ( r.metadata && r.metadata.interfaces &&
(r.metadata.macAddress && macs.includes(normalizeMac(r.metadata.macAddress))) || r.metadata.interfaces.some(i => i.mac && macs.includes(i.mac.toLowerCase()))
(r.metadata.interfaces && r.metadata.interfaces.some(i => macs.includes(normalizeMac(i.mac))))
)
); );
} }
} }
// 2. Fallback matching by IP address // Fallback matching by IP if no MAC match (weaker)
let ipsToMatch = []; let ipsToMatch = [];
if (res.metadata.interfaces) { if (res.metadata.interfaces) {
ipsToMatch = res.metadata.interfaces.map(i => i.ip).filter(i => !!i); ipsToMatch = res.metadata.interfaces.map(i => i.ip).filter(i => !!i);
} }
if (res.metadata.ip) ipsToMatch.push(res.metadata.ip);
if (res.metadata.address) { if (res.metadata.address) {
res.metadata.address.split(',').forEach(a => ipsToMatch.push(a.trim())); res.metadata.address.split(',').forEach(a => ipsToMatch.push(a.trim()));
} }
ipsToMatch = [...new Set(ipsToMatch.filter(Boolean))];
if (!existing && ipsToMatch.length > 0) { if (!existing && ipsToMatch.length > 0) {
const allRes = await Resource.list();
existing = allRes.find(r => { existing = allRes.find(r => {
if (!r.metadata) return false; if (!r.metadata) return false;
if (r.metadata.ip && ipsToMatch.includes(r.metadata.ip)) return true;
if (r.metadata.address) { if (r.metadata.address) {
const addrs = r.metadata.address.split(',').map(a => a.trim()); const addrs = r.metadata.address.split(',').map(a => a.trim());
if (addrs.some(a => ipsToMatch.includes(a))) return true; if (addrs.some(a => ipsToMatch.includes(a))) return true;
@@ -54,16 +47,13 @@ class DiscoveryReconciler {
}); });
} }
// 3. Fallback matching by Slug, Name, or Base Hostname // Fallback matching by Slug or Name
if (!existing && (res.slug || res.name)) { if (!existing && (res.slug || res.name)) {
const inputName = normalizeHost(res.name || res.slug); const allRes = await Resource.list();
existing = allRes.find(r => { existing = allRes.find(r =>
if (res.slug && r.slug === res.slug) return true; (res.slug && r.slug === res.slug) ||
if (res.name && r.name && r.name.toLowerCase() === res.name.toLowerCase()) return true; (res.name && r.name && r.name.toLowerCase() === res.name.toLowerCase())
if (inputName && r.name && normalizeHost(r.name) === inputName) return true; );
if (inputName && r.slug && normalizeHost(r.slug) === inputName) return true;
return false;
});
} }
if (existing) { if (existing) {
+1
View File
@@ -44,6 +44,7 @@ module.exports = {
{href: '/groups', icon: 'fas fa-users-cog', label: 'Groups', groups: ['app_sso_admin']}, {href: '/groups', icon: 'fas fa-users-cog', label: 'Groups', groups: ['app_sso_admin']},
{href: '/conf', icon: 'fas fa-cogs', label: 'Configuration', groups: ['app_sso_admin']}, {href: '/conf', icon: 'fas fa-cogs', label: 'Configuration', groups: ['app_sso_admin']},
{href: '/directory', icon: 'fa-solid fa-server', label: 'Directory', groups: ['app_sso_admin', 'app_sso_directory_admin', 'admin']}, {href: '/directory', icon: 'fa-solid fa-server', label: 'Directory', groups: ['app_sso_admin', 'app_sso_directory_admin', 'admin']},
{href: '/plugins', icon: 'fa-solid fa-plug', label: 'Plugins', groups: ['app_sso_admin', 'app_sso_directory_admin', 'admin']},
// Vault requires login - per-user secrets at secret/users/<uid>/*. // Vault requires login - per-user secrets at secret/users/<uid>/*.
{href: '/vault', icon: 'fa-solid fa-vault', label: 'Vault', groups: ['login']}, {href: '/vault', icon: 'fa-solid fa-vault', label: 'Vault', groups: ['login']},
{href: '/overview', icon: 'fa-solid fa-gauge-high', label: 'Overview', groups: ['app_sso_admin', 'admin']}, {href: '/overview', icon: 'fa-solid fa-gauge-high', label: 'Overview', groups: ['app_sso_admin', 'admin']},
+1 -2
View File
@@ -156,12 +156,11 @@ async function mintAppToken(name) {
// client's sso auth headers so OpenBao never sees them. // client's sso auth headers so OpenBao never sees them.
const VAULT_ADDR = process.env.VAULT_ADDR || 'http://openbao:8200'; const VAULT_ADDR = process.env.VAULT_ADDR || 'http://openbao:8200';
const ADMIN_GROUPS = ['app_sso_admin', 'app_super_admin', 'app_sso_directory_admin'];
const ADMIN_GROUP = 'app_sso_admin'; const ADMIN_GROUP = 'app_sso_admin';
async function isAdmin(user) { async function isAdmin(user) {
try { try {
await permission.byGroup(user, ADMIN_GROUPS); await permission.byGroup(user, [ADMIN_GROUP]);
return true; return true;
} catch (e) { } catch (e) {
return false; return false;
+235 -308
View File
@@ -2,14 +2,10 @@
<script type="text/javascript"> <script type="text/javascript">
app.auth.forceLogin(['admin', 'app_sso_admin']); app.auth.forceLogin(['admin', 'app_sso_admin']);
var messagingTypes = {};
var messagingPlugins = [];
$(document).ready(function() { $(document).ready(function() {
loadConf(); loadConf();
loadProxyConf(); loadProxyConf();
loadTos(); loadTos();
loadMessagingPlugins();
}); });
async function loadConf() { async function loadConf() {
@@ -80,7 +76,7 @@
} catch (error) { } catch (error) {
app.messages.toast('Failed to save configuration: ' + error.message, 'danger'); app.messages.toast('Failed to save configuration: ' + error.message, 'danger');
} finally { } finally {
btn.prop('disabled', false).html('<i class="fas fa-save me-1"></i> Save Configuration'); btn.prop('disabled', false).html('<i class="fas fa-save"></i> Save Configuration');
} }
} }
@@ -91,11 +87,13 @@
return; return;
} }
const btn = $('#btn-test-email'); const $inputGroup = $('#test-email-to').closest('.input-group');
const btn = $inputGroup.find('button');
const originalHtml = btn.html(); const originalHtml = btn.html();
btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Sending...'); btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Sending...');
try { try {
// First save the SMTP config, then send test email
const payload = { const payload = {
smtp: { smtp: {
host: $('#smtp-host').val(), host: $('#smtp-host').val(),
@@ -106,7 +104,11 @@
secure: $('#smtp-secure').is(':checked') secure: $('#smtp-secure').is(':checked')
} }
}; };
// Save config first
await app.api.post('conf', payload); await app.api.post('conf', payload);
// Then send test email
const result = await app.api.post('conf/test-email', { to }); const result = await app.api.post('conf/test-email', { to });
app.messages.toast(result.message || 'Test email sent!', 'success'); app.messages.toast(result.message || 'Test email sent!', 'success');
$('#test-email-to').val(''); $('#test-email-to').val('');
@@ -124,11 +126,13 @@
return; return;
} }
const btn = $('#btn-test-sms'); const $inputGroup = $('#test-sms-to').closest('.input-group');
const btn = $inputGroup.find('button');
const originalHtml = btn.html(); const originalHtml = btn.html();
btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Sending...'); btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Sending...');
try { try {
// First save the VoIP.ms config, then send test SMS
const payload = { const payload = {
voipms: { voipms: {
username: $('#voipms-username').val(), username: $('#voipms-username').val(),
@@ -136,7 +140,11 @@
password: $('#voipms-password').val() password: $('#voipms-password').val()
} }
}; };
// Save config first
await app.api.post('conf', payload); await app.api.post('conf', payload);
// Then send test SMS
const result = await app.api.post('conf/test-sms', { to }); const result = await app.api.post('conf/test-sms', { to });
app.messages.toast(result.message || 'Test SMS sent!', 'success'); app.messages.toast(result.message || 'Test SMS sent!', 'success');
$('#test-sms-to').val(''); $('#test-sms-to').val('');
@@ -193,18 +201,22 @@
} catch (error) { } catch (error) {
app.messages.toast('Failed to save Proxy configuration: ' + error.message, 'danger'); app.messages.toast('Failed to save Proxy configuration: ' + error.message, 'danger');
} finally { } finally {
btn.prop('disabled', false).html('<i class="fas fa-save me-1"></i> Save Proxy Secrets'); btn.prop('disabled', false).html('<i class="fas fa-save"></i> Save Proxy Secrets');
} }
} }
// ── Terms of Service editor ──────────────────────────────────────────
// Moved here from the admin Overview dashboard — it's a configuration
// control, so it belongs on the System Configuration page. The API is
// routes/tos.js (GET to read, PUT to save; PUT is app_sso_admin-gated, which
// matches this page's gate). app.tos.get/update are the shared frontend
// helpers (@simpleworkjs/frontend).
async function loadTos() { async function loadTos() {
try { try {
const tos = await app.tos.get(); const tos = await app.tos.get();
if (tos && tos.content) { document.getElementById('tos-content').value = tos.content;
document.getElementById('tos-content').value = tos.content; document.getElementById('tos-meta').textContent =
document.getElementById('tos-meta').textContent = 'Last updated ' + moment(tos.updated_on, 'x').fromNow() + ' by ' + tos.updated_by;
'Last updated ' + moment(tos.updated_on, 'x').fromNow() + ' by ' + tos.updated_by;
}
} catch(e) { } catch(e) {
console.error('Failed to load ToS:', e); console.error('Failed to load ToS:', e);
} }
@@ -236,332 +248,247 @@
loadTos(); loadTos();
}); });
} }
// ── Messaging Plugins ──────────────────────────────────────────────
function loadMessagingPlugins() {
app.api.get('plugins/types', function(err, res) {
if (!err && res && res.results) {
(res.results || []).forEach(t => { messagingTypes[t.type] = t; });
}
app.api.get('plugins', function(err, res) {
if (err) return;
messagingPlugins = (res.results || []).filter(p => p.category === 'messaging');
renderMessagingPlugins();
});
});
}
function renderMessagingPlugins() {
const $list = $('#messaging-plugins-list').empty();
if (messagingPlugins.length === 0) {
$list.append('<div class="text-muted text-center py-3"><i class="fas fa-plug text-secondary mb-2 fs-3"></i><br>No messaging plugins configured.</div>');
return;
}
messagingPlugins.forEach(p => {
const badgeClass = p.enabled ? 'bg-success' : 'bg-secondary';
const statusText = p.enabled ? 'Loaded' : 'Unloaded';
const card = `
<div class="card mb-3 border shadow-sm">
<div class="card-body d-flex align-items-center justify-content-between">
<div>
<h6 class="mb-1"><strong>${p.name}</strong> <span class="badge bg-secondary ms-2">${p.pluginType}</span></h6>
<div class="small text-muted font-monospace">${p.slug} | Schedule: ${p.cron}</div>
</div>
<div class="d-flex align-items-center gap-2">
<span class="badge ${badgeClass} me-2">${statusText}</span>
<button class="btn btn-sm btn-outline-primary" onclick="togglePlugin('${p.id}', ${!p.enabled})">${p.enabled ? 'Unload' : 'Load'}</button>
<button class="btn btn-sm btn-outline-danger" onclick="deletePlugin('${p.id}')"><i class="fas fa-trash"></i></button>
</div>
</div>
</div>
`;
$list.append(card);
});
}
async function togglePlugin(id, state) {
const endpoint = state ? 'load' : 'unload';
try {
await app.api.post(`plugins/${id}/${endpoint}`, {});
app.messages.toast(`Plugin ${state ? 'loaded' : 'unloaded'} successfully`, 'success');
loadMessagingPlugins();
} catch (e) {
app.messages.toast('Error toggling plugin: ' + e.message, 'danger');
}
}
async function deletePlugin(id) {
const ok = await app.messages.confirm('Are you sure you want to delete this plugin instance?');
if (!ok) return;
try {
await app.api.delete(`plugins/${id}`);
app.messages.toast('Plugin deleted', 'success');
loadMessagingPlugins();
} catch (e) {
app.messages.toast('Error deleting plugin: ' + e.message, 'danger');
}
}
</script> </script>
<div class="container-fluid py-4 px-md-4"> <div class="container py-4">
<!-- Page Header --> <div class="row mb-4">
<div class="d-flex justify-content-between align-items-center mb-4 pb-3 border-bottom"> <div class="col d-flex justify-content-between align-items-center">
<div> <div>
<h3 class="fw-bold mb-1"><i class="fas fa-sliders-h text-primary me-2"></i> System Configuration</h3> <h2><i class="fas fa-cogs"></i> System Configuration</h2>
<p class="text-muted mb-0 small"> <p class="text-muted mb-0">
Manage stack configuration (SMTP, OAuth, VoIP.ms, Proxy, Terms of Service, and Messaging Plugins). Secrets are stored in OpenBao. Manage runtime configuration such as SMTP, SMS, OAuth, and Terms of Service
</p> settings. These are stored securely in OpenBao and take effect immediately.
</div> Secret fields (the SMTP password, OAuth JWT secret, and VoIP.ms API password)
<div> are masked — leave them unchanged to keep the stored value.
<button class="btn btn-outline-secondary me-2" onclick="loadConf()"><i class="fas fa-rotate me-1"></i> Reset</button> </p>
<button id="btn-save" class="btn btn-primary" onclick="saveConf()"><i class="fas fa-save me-1"></i> Save Configuration</button> </div>
<div>
<button class="btn btn-secondary me-2" onclick="loadConf()"><i class="fas fa-undo"></i> Reset</button>
<button id="btn-save" class="btn btn-primary" onclick="saveConf()"><i class="fas fa-save"></i> Save Configuration</button>
</div>
</div> </div>
</div> </div>
<div class="row g-4"> <ul class="nav nav-tabs mb-4" id="confTabs" role="tablist">
<!-- Sidebar Navigation --> <li class="nav-item" role="presentation">
<div class="col-lg-3 col-md-4"> <button class="nav-link active" id="smtp-tab" data-bs-toggle="tab" data-bs-target="#smtp" type="button" role="tab">SMTP Settings</button>
<div class="card shadow-sm border-0 sticky-top" style="top: var(--sw-content-offset, 70px);"> </li>
<div class="list-group list-group-flush rounded-3" id="conf-nav-list" role="tablist"> <li class="nav-item" role="presentation">
<a class="list-group-item list-group-item-action active d-flex align-items-center py-3" id="tab-nav-oauth" data-bs-toggle="list" href="#pane-oauth" role="tab"> <button class="nav-link" id="oauth-tab" data-bs-toggle="tab" data-bs-target="#oauth" type="button" role="tab">OAuth & JWT</button>
<i class="fas fa-key text-success me-3 fs-5" style="width: 24px;"></i> </li>
<div> <li class="nav-item" role="presentation">
<div class="fw-semibold">OAuth & JWT</div> <button class="nav-link" id="sms-tab" data-bs-toggle="tab" data-bs-target="#sms" type="button" role="tab">SMS (VoIP.ms)</button>
<div class="small text-muted">Issuer & Token Lifetimes</div> </li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="tos-tab" data-bs-toggle="tab" data-bs-target="#tos" type="button" role="tab">Terms of Service</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="proxy-tab" data-bs-toggle="tab" data-bs-target="#proxy" type="button" role="tab">Proxy Secrets</button>
</li>
</ul>
<div class="tab-content" id="confTabsContent">
<!-- SMTP Tab -->
<div class="tab-pane fade show active" id="smtp" role="tabpanel">
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white border-bottom-0 pt-4 pb-0">
<h5 class="mb-0"><i class="fas fa-envelope text-primary me-2"></i> SMTP Settings</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Host</label>
<input type="text" class="form-control" id="smtp-host">
</div>
<div class="mb-3">
<label class="form-label">Port</label>
<input type="number" class="form-control" id="smtp-port">
</div>
<div class="mb-3">
<label class="form-label">User</label>
<input type="text" class="form-control" id="smtp-user">
</div>
<div class="mb-3">
<label class="form-label">Password</label>
<div class="input-group">
<input type="password" class="form-control" id="smtp-pass" placeholder="********">
<button class="btn btn-outline-secondary" type="button" onclick="togglePassword('smtp-pass')"><i class="fas fa-eye"></i></button>
</div> </div>
</a> <div class="form-text">Leave unchanged to keep the current password stored in OpenBao. Clear and type a new value to replace it.</div>
<a class="list-group-item list-group-item-action d-flex align-items-center py-3" id="tab-nav-smtp" data-bs-toggle="list" href="#pane-smtp" role="tab"> <hr class="my-4">
<i class="fas fa-envelope text-primary me-3 fs-5" style="width: 24px;"></i> <div class="mb-3">
<div> <label class="form-label">Send Test SMS</label>
<div class="fw-semibold">Email (SMTP)</div> <div class="input-group">
<div class="small text-muted">Mail Delivery & Testing</div> <input type="tel" class="form-control" id="test-sms-to" placeholder="+15551234567">
<button class="btn btn-outline-primary" type="button" onclick="sendTestSms()">
<i class="fas fa-paper-plane"></i> Send Test SMS
</button>
</div> </div>
</a> <div class="form-text">Send a test SMS to verify your VoIP.ms configuration is working.</div>
<a class="list-group-item list-group-item-action d-flex align-items-center py-3" id="tab-nav-sms" data-bs-toggle="list" href="#pane-sms" role="tab"> </div>
<i class="fas fa-comment-sms text-info me-3 fs-5" style="width: 24px;"></i> </div>
<div> <hr class="my-4">
<div class="fw-semibold">SMS & Messaging</div> <div class="mb-3">
<div class="small text-muted">VoIP.ms & Webhook Plugins</div> <label class="form-label">Send Test SMS</label>
<div class="input-group">
<input type="tel" class="form-control" id="test-sms-to" placeholder="+15551234567">
<button class="btn btn-outline-primary" type="button" onclick="sendTestSms()">
<i class="fas fa-paper-plane"></i> Send Test SMS
</button>
</div> </div>
</a> <div class="form-text">Send a test SMS to verify your VoIP.ms configuration is working.</div>
<a class="list-group-item list-group-item-action d-flex align-items-center py-3" id="tab-nav-proxy" data-bs-toggle="list" href="#pane-proxy" role="tab"> </div>
<i class="fas fa-shield-alt text-warning me-3 fs-5" style="width: 24px;"></i> <div class="mb-3">
<div> <label class="form-label">From Address</label>
<div class="fw-semibold">Proxy Secrets</div> <input type="text" class="form-control" id="smtp-from">
<div class="small text-muted">OpenBao Integration</div> </div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="smtp-secure">
<label class="form-check-label">Use Secure (TLS)</label>
</div>
<hr class="my-4">
<div class="mb-3">
<label class="form-label">Send Test Email</label>
<div class="input-group">
<input type="email" class="form-control" id="test-email-to" placeholder="recipient@example.com">
<button class="btn btn-outline-primary" type="button" onclick="sendTestEmail()">
<i class="fas fa-paper-plane"></i> Send Test Email
</button>
</div>
<div class="form-text">Send a test email to verify your SMTP configuration is working.</div>
</div> </div>
</a>
<a class="list-group-item list-group-item-action d-flex align-items-center py-3" id="tab-nav-tos" data-bs-toggle="list" href="#pane-tos" role="tab">
<i class="fas fa-file-contract text-secondary me-3 fs-5" style="width: 24px;"></i>
<div>
<div class="fw-semibold">Terms of Service</div>
<div class="small text-muted">User Agreement & Policy</div>
</div>
</a>
</div> </div>
</div> </div>
</div> </div>
<!-- Main Content Panes --> <!-- OAuth Tab -->
<div class="col-lg-9 col-md-8"> <div class="tab-pane fade" id="oauth" role="tabpanel">
<div class="tab-content" id="conf-tab-content"> <div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white border-bottom-0 pt-4 pb-0">
<!-- OAuth Pane --> <h5 class="mb-0"><i class="fas fa-key text-success me-2"></i> OAuth & JWT Settings</h5>
<div class="tab-pane fade show active" id="pane-oauth" role="tabpanel"> </div>
<div class="card shadow-sm border-0 mb-4"> <div class="card-body">
<div class="card-header bg-white pt-4 pb-3 border-bottom"> <div class="mb-3">
<h5 class="mb-0 fw-bold"><i class="fas fa-key text-success me-2"></i> OAuth 2.0 & JWT Settings</h5> <label class="form-label">Issuer URL</label>
</div> <input type="text" class="form-control" id="oauth-issuer">
<div class="card-body p-4"> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label fw-semibold">Issuer URL</label> <label class="form-label">JWT Secret</label>
<input type="text" class="form-control" id="oauth-issuer" placeholder="https://sso.example.com"> <div class="input-group">
</div> <input type="password" class="form-control" id="oauth-jwtsecret" placeholder="********">
<div class="mb-3"> <button class="btn btn-outline-secondary" type="button" onclick="togglePassword('oauth-jwtsecret')"><i class="fas fa-eye"></i></button>
<label class="form-label fw-semibold">JWT Secret</label>
<div class="input-group">
<input type="password" class="form-control" id="oauth-jwtsecret" placeholder="********">
<button class="btn btn-outline-secondary" type="button" onclick="togglePassword('oauth-jwtsecret')"><i class="fas fa-eye"></i></button>
</div>
<div class="form-text">Stored in OpenBao. Leave unchanged to preserve stored value.</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Access Token Lifetime (seconds)</label>
<input type="number" class="form-control" id="oauth-token-access" placeholder="3600">
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Refresh Token Lifetime (seconds)</label>
<input type="number" class="form-control" id="oauth-token-refresh" placeholder="2592000">
</div>
</div>
</div> </div>
<div class="form-text">Leave unchanged to keep the current secret stored in OpenBao. Clear and type a new value to replace it.</div>
</div>
<div class="mb-3">
<label class="form-label">Access Token Lifetime (seconds)</label>
<input type="number" class="form-control" id="oauth-token-access">
</div>
<div class="mb-3">
<label class="form-label">Refresh Token Lifetime (seconds)</label>
<input type="number" class="form-control" id="oauth-token-refresh">
</div> </div>
</div> </div>
</div>
</div>
<!-- SMTP Pane --> <!-- SMS Tab -->
<div class="tab-pane fade" id="pane-smtp" role="tabpanel"> <div class="tab-pane fade" id="sms" role="tabpanel">
<div class="card shadow-sm border-0 mb-4"> <div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white pt-4 pb-3 border-bottom"> <div class="card-header bg-white border-bottom-0 pt-4 pb-0">
<h5 class="mb-0 fw-bold"><i class="fas fa-envelope text-primary me-2"></i> SMTP Server Settings</h5> <h5 class="mb-0"><i class="fas fa-comment text-info me-2"></i> SMS (VoIP.ms)</h5>
</div>
<div class="card-body">
<p class="form-text">Used to deliver SMS 2FA login codes. The API password is stored in OpenBao and masked below.</p>
<div class="mb-3">
<label class="form-label">API Username</label>
<input type="text" class="form-control" id="voipms-username">
</div>
<div class="mb-3">
<label class="form-label">DID (sender number)</label>
<input type="text" class="form-control" id="voipms-did" placeholder="15551234567">
</div>
<div class="mb-3">
<label class="form-label">API Password</label>
<div class="input-group">
<input type="password" class="form-control" id="voipms-password" placeholder="********">
<button class="btn btn-outline-secondary" type="button" onclick="togglePassword('voipms-password')"><i class="fas fa-eye"></i></button>
</div> </div>
<div class="card-body p-4"> <div class="form-text">Leave unchanged to keep the current password stored in OpenBao. Clear and type a new value to replace it.</div>
<div class="row"> <hr class="my-4">
<div class="col-md-8 mb-3"> <div class="mb-3">
<label class="form-label fw-semibold">SMTP Host</label> <label class="form-label">Send Test SMS</label>
<input type="text" class="form-control" id="smtp-host" placeholder="smtp.example.com"> <div class="input-group">
</div> <input type="tel" class="form-control" id="test-sms-to" placeholder="+15551234567">
<div class="col-md-4 mb-3"> <button class="btn btn-outline-primary" type="button" onclick="sendTestSms()">
<label class="form-label fw-semibold">Port</label> <i class="fas fa-paper-plane"></i> Send Test SMS
<input type="number" class="form-control" id="smtp-port" placeholder="587"> </button>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">User</label>
<input type="text" class="form-control" id="smtp-user">
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Password</label>
<div class="input-group">
<input type="password" class="form-control" id="smtp-pass" placeholder="********">
<button class="btn btn-outline-secondary" type="button" onclick="togglePassword('smtp-pass')"><i class="fas fa-eye"></i></button>
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">From Address</label>
<input type="text" class="form-control" id="smtp-from" placeholder="noreply@example.com">
</div>
<div class="form-check mb-4">
<input class="form-check-input" type="checkbox" id="smtp-secure">
<label class="form-check-label fw-semibold" for="smtp-secure">Use Secure TLS Connection</label>
</div>
<div class="p-3 bg-light rounded border">
<h6 class="fw-bold mb-2"><i class="fas fa-paper-plane text-primary me-2"></i> Send Test Email</h6>
<div class="input-group">
<input type="email" class="form-control" id="test-email-to" placeholder="recipient@example.com">
<button id="btn-test-email" class="btn btn-outline-primary" type="button" onclick="sendTestEmail()">
<i class="fas fa-paper-plane me-1"></i> Send Test Email
</button>
</div>
<div class="form-text">Saves current SMTP config and sends a test message.</div>
</div>
</div> </div>
<div class="form-text">Send a test SMS to verify your VoIP.ms configuration is working.</div>
</div> </div>
</div> </div>
</div>
</div>
<!-- SMS & Messaging Pane --> <!-- Proxy Secrets Tab -->
<div class="tab-pane fade" id="pane-sms" role="tabpanel"> <div class="tab-pane fade" id="proxy" role="tabpanel">
<div class="card shadow-sm border-0 mb-4"> <div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white pt-4 pb-3 border-bottom"> <div class="card-header bg-white border-bottom-0 pt-4 pb-0">
<h5 class="mb-0 fw-bold"><i class="fas fa-comment-sms text-info me-2"></i> VoIP.ms SMS Integration</h5> <h5 class="mb-0"><i class="fas fa-shield-alt text-warning me-2"></i> Proxy Secrets (OpenBao)</h5>
</div> </div>
<div class="card-body p-4"> <div class="card-body">
<div class="row"> <p class="form-text">These secrets are stored directly in OpenBao (`secret/proxy/conf`) and read by the Proxy at boot.</p>
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">API Username</label>
<input type="text" class="form-control" id="voipms-username">
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">DID Sender Number</label>
<input type="text" class="form-control" id="voipms-did" placeholder="15551234567">
</div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">API Password</label>
<div class="input-group">
<input type="password" class="form-control" id="voipms-password" placeholder="********">
<button class="btn btn-outline-secondary" type="button" onclick="togglePassword('voipms-password')"><i class="fas fa-eye"></i></button>
</div>
</div>
<div class="p-3 bg-light rounded border mb-4"> <h6 class="mt-3 mb-2">OAuth / OIDC Integration</h6>
<h6 class="fw-bold mb-2"><i class="fas fa-paper-plane text-info me-2"></i> Send Test SMS</h6> <div class="mb-3">
<div class="input-group"> <label class="form-label">Issuer URL</label>
<input type="tel" class="form-control" id="test-sms-to" placeholder="+15551234567"> <input type="text" class="form-control" id="proxy-issuer" placeholder="https://sso.example.com">
<button id="btn-test-sms" class="btn btn-outline-info" type="button" onclick="sendTestSms()"> </div>
<i class="fas fa-paper-plane me-1"></i> Send Test SMS <div class="mb-3">
</button> <label class="form-label">Client ID</label>
</div> <input type="text" class="form-control" id="proxy-client-id">
</div> </div>
<div class="mb-3">
<hr class="my-4"> <label class="form-label">Client Secret</label>
<div class="input-group">
<div class="d-flex justify-content-between align-items-center mb-3"> <input type="password" class="form-control" id="proxy-client-secret" placeholder="********">
<h5 class="mb-0 fw-bold"><i class="fas fa-plug text-primary me-2"></i> Messaging Plugins & Webhooks</h5> <button class="btn btn-outline-secondary" type="button" onclick="togglePassword('proxy-client-secret')"><i class="fas fa-eye"></i></button>
<button class="btn btn-sm btn-outline-primary" onclick="loadMessagingPlugins()"><i class="fas fa-rotate"></i> Refresh</button>
</div>
<div id="messaging-plugins-list"></div>
</div> </div>
</div> </div>
</div>
<!-- Proxy Secrets Pane --> <h6 class="mt-4 mb-2">LDAP Integration</h6>
<div class="tab-pane fade" id="pane-proxy" role="tabpanel"> <div class="mb-3">
<div class="card shadow-sm border-0 mb-4"> <label class="form-label">Bind Password</label>
<div class="card-header bg-white pt-4 pb-3 border-bottom"> <div class="input-group">
<h5 class="mb-0 fw-bold"><i class="fas fa-shield-alt text-warning me-2"></i> OpenBao Proxy Integration</h5> <input type="password" class="form-control" id="proxy-ldap-bindpass" placeholder="********">
</div> <button class="btn btn-outline-secondary" type="button" onclick="togglePassword('proxy-ldap-bindpass')"><i class="fas fa-eye"></i></button>
<div class="card-body p-4">
<p class="text-muted small">Secrets stored directly in OpenBao (<code>secret/proxy/conf</code>) and consumed by Proxy at boot.</p>
<h6 class="fw-bold text-dark mt-3 mb-2">OAuth / OIDC Client</h6>
<div class="mb-3">
<label class="form-label fw-semibold">Issuer URL</label>
<input type="text" class="form-control" id="proxy-issuer" placeholder="https://sso.example.com">
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Client ID</label>
<input type="text" class="form-control" id="proxy-client-id">
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">Client Secret</label>
<div class="input-group">
<input type="password" class="form-control" id="proxy-client-secret" placeholder="********">
<button class="btn btn-outline-secondary" type="button" onclick="togglePassword('proxy-client-secret')"><i class="fas fa-eye"></i></button>
</div>
</div>
</div>
<h6 class="fw-bold text-dark mt-4 mb-2">LDAP Bind Account</h6>
<div class="mb-3">
<label class="form-label fw-semibold">Proxy Bind Password</label>
<div class="input-group">
<input type="password" class="form-control" id="proxy-ldap-bindpass" placeholder="********">
<button class="btn btn-outline-secondary" type="button" onclick="togglePassword('proxy-ldap-bindpass')"><i class="fas fa-eye"></i></button>
</div>
</div>
<button id="btn-save-proxy" class="btn btn-warning mt-2 text-dark fw-semibold" onclick="saveProxyConf()"><i class="fas fa-save me-1"></i> Save Proxy Secrets</button>
</div> </div>
<div class="form-text">Password for the Proxy's LDAP service account.</div>
</div> </div>
</div>
<!-- Terms of Service Pane --> <button id="btn-save-proxy" class="btn btn-warning mt-2" onclick="saveProxyConf()"><i class="fas fa-save"></i> Save Proxy Secrets</button>
<div class="tab-pane fade" id="pane-tos" role="tabpanel"> </div>
<div class="card shadow-sm border-0 mb-4"> </div>
<div class="card-header bg-white pt-4 pb-3 border-bottom d-flex justify-content-between align-items-center"> </div>
<h5 class="mb-0 fw-bold"><i class="fas fa-file-contract me-2"></i> Terms of Service Editor</h5>
<span class="small text-muted" id="tos-meta"></span> <!-- ToS Tab -->
</div> <div class="tab-pane fade" id="tos" role="tabpanel">
<div class="card-body p-4"> <div class="card shadow-sm border-0 mb-4">
<div class="mb-3"> <div class="card-header bg-white border-bottom-0 pt-4 pb-0 d-flex justify-content-between align-items-center">
<label class="form-label fw-semibold">Terms Content (Markdown)</label> <h5 class="mb-0"><i class="fas fa-file-contract me-2"></i> Terms of Service</h5>
<textarea class="form-control font-monospace" id="tos-content" rows="10" placeholder="Enter Terms of Service markdown content..."></textarea> <small class="text-muted" id="tos-meta"></small>
</div> </div>
<div class="form-check mb-4"> <div class="card-body">
<input class="form-check-input" type="checkbox" id="tos-reset-acceptance"> <div class="mb-3">
<label class="form-check-label fw-semibold" for="tos-reset-acceptance">Require all users to re-accept these terms upon next login</label> <label class="form-label">Content <small class="text-muted">(Markdown)</small></label>
</div> <textarea class="form-control" id="tos-content" rows="8"></textarea>
<button class="btn btn-primary" onclick="saveTos()"><i class="fas fa-floppy-disk me-1"></i> Save Terms of Service</button>
<div id="tos-result" style="display:none" class="mt-3"></div>
</div>
</div> </div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="tos-reset-acceptance">
<label class="form-check-label" for="tos-reset-acceptance">Require all users to re-accept these terms</label>
</div>
<button class="btn btn-primary" onclick="saveTos()"><i class="fas fa-floppy-disk"></i> Save Terms</button>
<div id="tos-result" style="display:none" class="mt-2"></div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
+5 -95
View File
@@ -13,12 +13,7 @@
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link" id="discovery-tab" data-bs-toggle="tab" data-bs-target="#discovery-tab-pane" type="button" role="tab" aria-controls="discovery-tab-pane" aria-selected="false"> <button class="nav-link" id="discovery-tab" data-bs-toggle="tab" data-bs-target="#discovery-tab-pane" type="button" role="tab" aria-controls="discovery-tab-pane" aria-selected="false">
<i class="fa-solid fa-network-wired"></i> Discovered Inventory <i class="fa-solid fa-network-wired"></i> Discovery
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="plugins-tab" data-bs-toggle="tab" data-bs-target="#plugins-tab-pane" type="button" role="tab" aria-controls="plugins-tab-pane" aria-selected="false">
<i class="fa-solid fa-plug"></i> Discovery Plugins
</button> </button>
</li> </li>
</ul> </ul>
@@ -192,20 +187,6 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Discovery Plugins Tab Pane -->
<div class="tab-pane fade" id="plugins-tab-pane" role="tabpanel" aria-labelledby="plugins-tab">
<div class="p-4 bg-white border-top">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h5 class="fw-bold mb-1"><i class="fa-solid fa-plug text-primary me-2"></i> Discovery Plugins</h5>
<p class="text-muted small mb-0">Manage background discovery agents (Nmap, Docker, Proxmox, UniFi). Per-instance secrets are stored in OpenBao.</p>
</div>
<button class="btn btn-sm btn-outline-primary" onclick="loadDiscoveryPlugins()"><i class="fas fa-rotate me-1"></i> Refresh</button>
</div>
<div id="discovery-plugins-list" class="mt-3"></div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -1207,7 +1188,6 @@
allEdges.push(res.results); allEdges.push(res.results);
refreshEdgesUI(resourceId); refreshEdgesUI(resourceId);
$('#new-edge-target').val(''); $('#new-edge-target').val('');
await loadData();
} catch (err) { } catch (err) {
console.error(err); console.error(err);
app.messages.action('Failed to add edge', app.modal.body(), 'danger'); app.messages.action('Failed to add edge', app.modal.body(), 'danger');
@@ -1219,7 +1199,6 @@
await app.api.delete('directory-admin/edges/' + id); await app.api.delete('directory-admin/edges/' + id);
allEdges = allEdges.filter(e => e.id !== id); allEdges = allEdges.filter(e => e.id !== id);
refreshEdgesUI($('#res-id').val()); refreshEdgesUI($('#res-id').val());
await loadData();
} catch (err) { } catch (err) {
console.error(err); console.error(err);
app.messages.action('Failed to remove edge', app.modal.body(), 'danger'); app.messages.action('Failed to remove edge', app.modal.body(), 'danger');
@@ -1514,7 +1493,7 @@
`; `;
app.modal.open({ app.modal.open({
title: 'Install Theta Agent', title: '<i class="fa-solid fa-shield-halved text-primary me-2"></i> Install Theta Agent',
bodyHtml: bodyHtml, bodyHtml: bodyHtml,
size: 'lg' size: 'lg'
}); });
@@ -1522,81 +1501,12 @@
updateAgentCommands(); updateAgentCommands();
} }
var discoveryPlugins = []; // Plugin scheduling moved to the dedicated /plugins page (the Agents &
// Scheduler tab here was its old home). Discovery inventory + the discovery
function loadDiscoveryPlugins() { // results table remain on this page.
app.api.get('plugins', function(err, res) {
if (err) return;
discoveryPlugins = (res.results || []).filter(p => p.category === 'discovery');
renderDiscoveryPlugins();
});
}
function renderDiscoveryPlugins() {
const $list = $('#discovery-plugins-list').empty();
if (discoveryPlugins.length === 0) {
$list.append('<div class="text-muted text-center py-4"><i class="fa-solid fa-plug fs-2 mb-2 text-black-50"></i><br>No discovery plugins configured.</div>');
return;
}
discoveryPlugins.forEach(p => {
const badgeClass = p.enabled ? 'bg-success' : 'bg-secondary';
const statusText = p.enabled ? 'Loaded' : 'Unloaded';
const card = `
<div class="card mb-3 border shadow-sm">
<div class="card-body d-flex align-items-center justify-content-between">
<div>
<h6 class="mb-1"><strong>${p.name}</strong> <span class="badge bg-secondary ms-2">${p.pluginType}</span></h6>
<div class="small text-muted font-monospace">${p.slug} | Schedule: ${p.cron}</div>
</div>
<div class="d-flex align-items-center gap-2">
<span class="badge ${badgeClass} me-2">${statusText}</span>
<button class="btn btn-sm btn-outline-primary" onclick="toggleDiscoveryPlugin('${p.id}', ${!p.enabled})">${p.enabled ? 'Unload' : 'Load'}</button>
<button class="btn btn-sm btn-success" title="Run now" onclick="runDiscoveryPluginNow('${p.id}')"><i class="fa-solid fa-play"></i> Run</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteDiscoveryPlugin('${p.id}')"><i class="fas fa-trash"></i></button>
</div>
</div>
</div>
`;
$list.append(card);
});
}
async function toggleDiscoveryPlugin(id, state) {
const endpoint = state ? 'load' : 'unload';
try {
await app.api.post(`plugins/${id}/${endpoint}`, {});
app.messages.toast(`Discovery plugin ${state ? 'loaded' : 'unloaded'}`, 'success');
loadDiscoveryPlugins();
} catch (e) {
app.messages.toast('Error toggling plugin: ' + e.message, 'danger');
}
}
async function runDiscoveryPluginNow(id) {
try {
await app.api.post(`plugins/${id}/run`, {});
app.messages.toast('Enqueued discovery plugin run', 'success');
loadDiscoveryPlugins();
} catch (e) {
app.messages.toast('Error running plugin: ' + e.message, 'danger');
}
}
async function deleteDiscoveryPlugin(id) {
const ok = await app.messages.confirm('Are you sure you want to delete this discovery plugin?');
if (!ok) return;
try {
await app.api.delete(`plugins/${id}`);
app.messages.toast('Discovery plugin deleted', 'success');
loadDiscoveryPlugins();
} catch (e) {
app.messages.toast('Error deleting plugin: ' + e.message, 'danger');
}
}
$(document).ready(function(){ $(document).ready(function(){
loadDiscoveryResources(); loadDiscoveryResources();
loadDiscoveryPlugins();
}); });
</script> </script>