Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2612b0e3ab | |||
| bf471c2e19 | |||
| d802c399a3 | |||
| d8242b1d53 |
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "t42-sso-manager",
|
"name": "t42-sso-manager",
|
||||||
"version": "1.20.1",
|
"version": "1.20.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "t42-sso-manager",
|
"name": "t42-sso-manager",
|
||||||
"version": "1.20.0",
|
"version": "1.20.2",
|
||||||
"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.20.1",
|
"version": "1.20.2",
|
||||||
"description": "A very simple LDAP management and SSO system",
|
"description": "A very simple LDAP management and SSO system",
|
||||||
"author": [
|
"author": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ describe('vault_broker admin policy', () => {
|
|||||||
return { status: 404, text: async () => '' };
|
return { status: 404, text: async () => '' };
|
||||||
}
|
}
|
||||||
if (method === 'PUT' && path === 'sys/policies/acl/sso-admin') {
|
if (method === 'PUT' && path === 'sys/policies/acl/sso-admin') {
|
||||||
expect(body.policy).toContain('path "secret/metadata" { capabilities = ["list", "read", "delete"] }');
|
expect(body.policy).toContain('path "secret/metadata" { capabilities = ["create", "read", "update", "delete", "list"] }');
|
||||||
expect(body.policy).toContain('path "secret/metadata/" { capabilities = ["list", "read", "delete"] }');
|
expect(body.policy).toContain('path "secret/metadata/" { capabilities = ["create", "read", "update", "delete", "list"] }');
|
||||||
return { status: 204, ok: true };
|
return { status: 204, ok: true };
|
||||||
}
|
}
|
||||||
if (method === 'POST' && path === 'auth/token/create/sso-broker') {
|
if (method === 'POST' && path === 'auth/token/create/sso-broker') {
|
||||||
|
|||||||
@@ -79,17 +79,19 @@ async function mintToken(policies) {
|
|||||||
return { token, ttl };
|
return { token, ttl };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Per-user token ──────────────────────────────────────────────────────────
|
||||||
// ── Per-user token ──────────────────────────────────────────────────────────
|
// ── Per-user token ──────────────────────────────────────────────────────────
|
||||||
function userPolicyHcl(uid) {
|
function userPolicyHcl(uid) {
|
||||||
// uid is an LDAP uid (alphanumeric + a few separators); it is interpolated
|
return `path "secret/data/users/${uid}" { capabilities = ["create", "read", "update", "delete", "list"] }
|
||||||
// into a policy path, so reject anything but a safe charset.
|
path "secret/data/users/${uid}/*" { capabilities = ["create", "read", "update", "delete", "list"] }
|
||||||
// The bare `secret/metadata/users/<uid>` grant is required to LIST the
|
path "secret/metadata/users/${uid}" { capabilities = ["create", "read", "update", "delete", "list"] }
|
||||||
// contents of the namespace: `.../*` covers nested paths but NOT the
|
path "secret/metadata/users/${uid}/" { capabilities = ["create", "read", "update", "delete", "list"] }
|
||||||
// directory itself, so without it the /vault secrets list 403s.
|
path "secret/metadata/users/${uid}/*" { capabilities = ["create", "read", "update", "delete", "list"] }
|
||||||
return `path "secret/data/users/${uid}/*" { capabilities = ["create", "read", "update", "delete", "list"] }
|
path "secret/data/shared" { capabilities = ["read", "list"] }
|
||||||
path "secret/metadata/users/${uid}" { capabilities = ["list", "read", "delete"] }
|
path "secret/data/shared/*" { capabilities = ["read", "list"] }
|
||||||
path "secret/metadata/users/${uid}/" { capabilities = ["list", "read", "delete"] }
|
path "secret/metadata/shared" { capabilities = ["read", "list"] }
|
||||||
path "secret/metadata/users/${uid}/*" { capabilities = ["list", "read", "delete"] }`;
|
path "secret/metadata/shared/" { capabilities = ["read", "list"] }
|
||||||
|
path "secret/metadata/shared/*" { capabilities = ["read", "list"] }`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mint (or return the cached) per-user token confined to secret/users/<uid>/*.
|
// Mint (or return the cached) per-user token confined to secret/users/<uid>/*.
|
||||||
@@ -107,13 +109,13 @@ async function getOrCreateUserToken(uid) {
|
|||||||
|
|
||||||
// ── Admin token (read/write all of secret/) ─────────────────────────────────
|
// ── Admin token (read/write all of secret/) ─────────────────────────────────
|
||||||
function adminPolicyHcl() {
|
function adminPolicyHcl() {
|
||||||
// The bare `secret/metadata` / `secret/metadata/` grants let an admin LIST
|
return `path "secret/*" { capabilities = ["create", "read", "update", "delete", "list"] }
|
||||||
// the KV mount root (the top-level dirs); `secret/metadata/*` covers nested
|
path "secret" { capabilities = ["create", "read", "update", "delete", "list"] }
|
||||||
// paths but NOT the root itself, so without it the /vault secrets list 403s.
|
path "secret/data/*" { capabilities = ["create", "read", "update", "delete", "list"] }
|
||||||
return `path "secret/data/*" { capabilities = ["create", "read", "update", "delete", "list"] }
|
path "secret/data" { capabilities = ["create", "read", "update", "delete", "list"] }
|
||||||
path "secret/metadata" { capabilities = ["list", "read", "delete"] }
|
path "secret/metadata" { capabilities = ["create", "read", "update", "delete", "list"] }
|
||||||
path "secret/metadata/" { capabilities = ["list", "read", "delete"] }
|
path "secret/metadata/" { capabilities = ["create", "read", "update", "delete", "list"] }
|
||||||
path "secret/metadata/*" { capabilities = ["list", "read", "delete"] }`;
|
path "secret/metadata/*" { capabilities = ["create", "read", "update", "delete", "list"] }`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getOrCreateAdminToken(uid) {
|
async function getOrCreateAdminToken(uid) {
|
||||||
@@ -128,11 +130,16 @@ async function getOrCreateAdminToken(uid) {
|
|||||||
|
|
||||||
// ── Per-app token (minted ONCE, returned to the caller, never cached) ───────
|
// ── Per-app token (minted ONCE, returned to the caller, never cached) ───────
|
||||||
function appPolicyHcl(name) {
|
function appPolicyHcl(name) {
|
||||||
// The bare `secret/metadata/apps/<name>` grant lets an app LIST its own
|
return `path "secret/data/apps/${name}" { capabilities = ["create", "read", "update", "delete", "list"] }
|
||||||
// namespace root (see userPolicyHcl for why `/*` alone isn't enough).
|
path "secret/data/apps/${name}/*" { capabilities = ["create", "read", "update", "delete", "list"] }
|
||||||
return `path "secret/data/apps/${name}/*" { capabilities = ["create", "read", "update", "delete", "list"] }
|
path "secret/metadata/apps/${name}" { capabilities = ["create", "read", "update", "delete", "list"] }
|
||||||
path "secret/metadata/apps/${name}" { capabilities = ["list", "read", "delete"] }
|
path "secret/metadata/apps/${name}/" { capabilities = ["create", "read", "update", "delete", "list"] }
|
||||||
path "secret/metadata/apps/${name}/*" { capabilities = ["list", "read", "delete"] }`;
|
path "secret/metadata/apps/${name}/*" { capabilities = ["create", "read", "update", "delete", "list"] }
|
||||||
|
path "secret/data/shared" { capabilities = ["read", "list"] }
|
||||||
|
path "secret/data/shared/*" { capabilities = ["read", "list"] }
|
||||||
|
path "secret/metadata/shared" { capabilities = ["read", "list"] }
|
||||||
|
path "secret/metadata/shared/" { capabilities = ["read", "list"] }
|
||||||
|
path "secret/metadata/shared/*" { capabilities = ["read", "list"] }`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the app-<name> policy + mint a token for it. Returns the token ONCE
|
// Create the app-<name> policy + mint a token for it. Returns the token ONCE
|
||||||
@@ -190,17 +197,13 @@ async function scopeGuard(req, res, next) {
|
|||||||
return res.status(503).json({ error: 'vault broker unavailable', detail: e.message });
|
return res.status(503).json({ error: 'vault broker unavailable', detail: e.message });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Defense-in-depth: confirm the requested path is within the subject's
|
|
||||||
// namespace. Admins roam all of secret/; users are confined to
|
|
||||||
// secret/users/<uid>/. (The token's own policy enforces the same at the
|
|
||||||
// OpenBao layer; this catches a buggy/malicious client early with a clear
|
|
||||||
// 403 instead of an opaque OpenBao denial.)
|
|
||||||
const norm = normalizeVaultPath(req.path);
|
const norm = normalizeVaultPath(req.path);
|
||||||
if (norm === null) {
|
if (norm === null) {
|
||||||
return res.status(403).json({ error: 'vault paths must be under /secret/' });
|
return res.status(403).json({ error: 'vault paths must be under /secret/' });
|
||||||
}
|
}
|
||||||
const base = `/secret/users/${uid}`;
|
const userBase = `/secret/users/${uid}`;
|
||||||
const allowed = admin || norm === base || norm.startsWith(base + '/');
|
const sharedBase = `/secret/shared`;
|
||||||
|
const allowed = admin || norm === userBase || norm.startsWith(userBase + '/') || norm === sharedBase || norm.startsWith(sharedBase + '/');
|
||||||
if (!allowed) {
|
if (!allowed) {
|
||||||
return res.status(403).json({ error: 'path outside your vault namespace' });
|
return res.status(403).json({ error: 'path outside your vault namespace' });
|
||||||
}
|
}
|
||||||
|
|||||||
+68
-111
@@ -1,4 +1,5 @@
|
|||||||
<%- include('top') %>
|
<%- include('top') %>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
app.auth.forceLogin(['admin', 'app_sso_admin']);
|
app.auth.forceLogin(['admin', 'app_sso_admin']);
|
||||||
|
|
||||||
@@ -48,7 +49,7 @@
|
|||||||
|
|
||||||
async function saveConf() {
|
async function saveConf() {
|
||||||
const btn = $('#btn-save');
|
const btn = $('#btn-save');
|
||||||
btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Saving...');
|
btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin me-1"></i> Saving...');
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
smtp: {
|
smtp: {
|
||||||
@@ -76,7 +77,7 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await app.api.post('conf', payload);
|
await app.api.post('conf', payload);
|
||||||
app.messages.toast('Configuration saved successfully! It will take effect immediately.', 'success');
|
app.messages.toast('Configuration saved successfully!', 'success');
|
||||||
} 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 {
|
||||||
@@ -93,7 +94,7 @@
|
|||||||
|
|
||||||
const btn = $('#btn-test-email');
|
const btn = $('#btn-test-email');
|
||||||
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 me-1"></i> Sending...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
@@ -126,7 +127,7 @@
|
|||||||
|
|
||||||
const btn = $('#btn-test-sms');
|
const btn = $('#btn-test-sms');
|
||||||
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 me-1"></i> Sending...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
@@ -174,7 +175,7 @@
|
|||||||
|
|
||||||
async function saveProxyConf() {
|
async function saveProxyConf() {
|
||||||
const btn = $('#btn-save-proxy');
|
const btn = $('#btn-save-proxy');
|
||||||
btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Saving...');
|
btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin me-1"></i> Saving...');
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
oidc: {
|
oidc: {
|
||||||
@@ -254,7 +255,7 @@
|
|||||||
function renderMessagingPlugins() {
|
function renderMessagingPlugins() {
|
||||||
const $list = $('#messaging-plugins-list').empty();
|
const $list = $('#messaging-plugins-list').empty();
|
||||||
if (messagingPlugins.length === 0) {
|
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>');
|
$list.append('<div class="text-muted text-center py-4"><i class="fas fa-plug text-black-50 fs-2 mb-2"></i><br>No messaging plugins configured.</div>');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
messagingPlugins.forEach(p => {
|
messagingPlugins.forEach(p => {
|
||||||
@@ -303,76 +304,52 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container-fluid py-4 px-md-4">
|
<div class="container mt-4">
|
||||||
<!-- Page Header -->
|
<div class="row">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4 pb-3 border-bottom">
|
<div class="col-12">
|
||||||
<div>
|
<div class="card shadow">
|
||||||
<h3 class="fw-bold mb-1"><i class="fas fa-sliders-h text-primary me-2"></i> System Configuration</h3>
|
<!-- Header with Sub-Nav Tabs matching directory.ejs -->
|
||||||
<p class="text-muted mb-0 small">
|
<div class="card-header d-flex flex-wrap justify-content-between align-items-center gap-2">
|
||||||
Manage stack configuration (SMTP, OAuth, VoIP.ms, Proxy, Terms of Service, and Messaging Plugins). Secrets are stored in OpenBao.
|
<ul class="nav nav-tabs card-header-tabs" id="confTabs" role="tablist">
|
||||||
</p>
|
<li class="nav-item" role="presentation">
|
||||||
</div>
|
<button class="nav-link active" id="oauth-tab" data-bs-toggle="tab" data-bs-target="#pane-oauth" type="button" role="tab">
|
||||||
<div>
|
<i class="fas fa-key text-success me-1"></i> OAuth & JWT
|
||||||
<button class="btn btn-outline-secondary me-2" onclick="loadConf()"><i class="fas fa-rotate me-1"></i> Reset</button>
|
</button>
|
||||||
<button id="btn-save" class="btn btn-primary" onclick="saveConf()"><i class="fas fa-save me-1"></i> Save Configuration</button>
|
</li>
|
||||||
</div>
|
<li class="nav-item" role="presentation">
|
||||||
</div>
|
<button class="nav-link" id="smtp-tab" data-bs-toggle="tab" data-bs-target="#pane-smtp" type="button" role="tab">
|
||||||
|
<i class="fas fa-envelope text-primary me-1"></i> Email (SMTP)
|
||||||
<div class="row g-4">
|
</button>
|
||||||
<!-- Sidebar Navigation -->
|
</li>
|
||||||
<div class="col-lg-3 col-md-4">
|
<li class="nav-item" role="presentation">
|
||||||
<div class="card shadow-sm border-0 sticky-top" style="top: var(--sw-content-offset, 70px);">
|
<button class="nav-link" id="sms-tab" data-bs-toggle="tab" data-bs-target="#pane-sms" type="button" role="tab">
|
||||||
<div class="list-group list-group-flush rounded-3" id="conf-nav-list" role="tablist">
|
<i class="fas fa-comment-sms text-info me-1"></i> SMS & Messaging
|
||||||
<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>
|
||||||
<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="proxy-tab" data-bs-toggle="tab" data-bs-target="#pane-proxy" type="button" role="tab">
|
||||||
<div class="small text-muted">Issuer & Token Lifetimes</div>
|
<i class="fas fa-shield-alt text-warning me-1"></i> Proxy Secrets
|
||||||
</div>
|
</button>
|
||||||
</a>
|
</li>
|
||||||
<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">
|
<li class="nav-item" role="presentation">
|
||||||
<i class="fas fa-envelope text-primary me-3 fs-5" style="width: 24px;"></i>
|
<button class="nav-link" id="tos-tab" data-bs-toggle="tab" data-bs-target="#pane-tos" type="button" role="tab">
|
||||||
<div>
|
<i class="fas fa-file-contract text-secondary me-1"></i> Terms of Service
|
||||||
<div class="fw-semibold">Email (SMTP)</div>
|
</button>
|
||||||
<div class="small text-muted">Mail Delivery & Testing</div>
|
</li>
|
||||||
</div>
|
</ul>
|
||||||
</a>
|
<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">
|
<button class="btn btn-sm btn-outline-secondary me-1" onclick="loadConf()"><i class="fas fa-rotate me-1"></i> Reset</button>
|
||||||
<i class="fas fa-comment-sms text-info me-3 fs-5" style="width: 24px;"></i>
|
<button id="btn-save" class="btn btn-sm btn-primary" onclick="saveConf()"><i class="fas fa-save me-1"></i> Save Configuration</button>
|
||||||
<div>
|
</div>
|
||||||
<div class="fw-semibold">SMS & Messaging</div>
|
|
||||||
<div class="small text-muted">VoIP.ms & Webhook Plugins</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<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">
|
|
||||||
<i class="fas fa-shield-alt text-warning me-3 fs-5" style="width: 24px;"></i>
|
|
||||||
<div>
|
|
||||||
<div class="fw-semibold">Proxy Secrets</div>
|
|
||||||
<div class="small text-muted">OpenBao Integration</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>
|
|
||||||
|
|
||||||
<!-- Main Content Panes -->
|
<div class="card-body p-4">
|
||||||
<div class="col-lg-9 col-md-8">
|
<div class="tab-content" id="confTabContent">
|
||||||
<div class="tab-content" id="conf-tab-content">
|
|
||||||
|
|
||||||
<!-- OAuth Pane -->
|
<!-- OAuth & JWT Tab -->
|
||||||
<div class="tab-pane fade show active" id="pane-oauth" role="tabpanel">
|
<div class="tab-pane fade show active" id="pane-oauth" role="tabpanel">
|
||||||
<div class="card shadow-sm border-0 mb-4">
|
<h5 class="fw-bold mb-3"><i class="fas fa-key text-success me-2"></i> OAuth 2.0 & JWT Settings</h5>
|
||||||
<div class="card-header bg-white pt-4 pb-3 border-bottom">
|
<p class="text-muted small">Configure OIDC issuer URLs, token lifetimes, and JWT signing keys. Stored in OpenBao.</p>
|
||||||
<h5 class="mb-0 fw-bold"><i class="fas fa-key text-success me-2"></i> OAuth 2.0 & JWT Settings</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body p-4">
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label fw-semibold">Issuer URL</label>
|
<label class="form-label fw-semibold">Issuer URL</label>
|
||||||
<input type="text" class="form-control" id="oauth-issuer" placeholder="https://sso.example.com">
|
<input type="text" class="form-control" id="oauth-issuer" placeholder="https://sso.example.com">
|
||||||
@@ -396,16 +373,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- SMTP Pane -->
|
<!-- SMTP Tab -->
|
||||||
<div class="tab-pane fade" id="pane-smtp" role="tabpanel">
|
<div class="tab-pane fade" id="pane-smtp" role="tabpanel">
|
||||||
<div class="card shadow-sm border-0 mb-4">
|
<h5 class="fw-bold mb-3"><i class="fas fa-envelope text-primary me-2"></i> SMTP Server Settings</h5>
|
||||||
<div class="card-header bg-white pt-4 pb-3 border-bottom">
|
<p class="text-muted small">System mail server credentials for password resets, notifications, and verification emails.</p>
|
||||||
<h5 class="mb-0 fw-bold"><i class="fas fa-envelope text-primary me-2"></i> SMTP Server Settings</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body p-4">
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-8 mb-3">
|
<div class="col-md-8 mb-3">
|
||||||
<label class="form-label fw-semibold">SMTP Host</label>
|
<label class="form-label fw-semibold">SMTP Host</label>
|
||||||
@@ -449,16 +421,11 @@
|
|||||||
<div class="form-text">Saves current SMTP config and sends a test message.</div>
|
<div class="form-text">Saves current SMTP config and sends a test message.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- SMS & Messaging Pane -->
|
<!-- SMS & Messaging Tab -->
|
||||||
<div class="tab-pane fade" id="pane-sms" role="tabpanel">
|
<div class="tab-pane fade" id="pane-sms" role="tabpanel">
|
||||||
<div class="card shadow-sm border-0 mb-4">
|
<h5 class="fw-bold mb-3"><i class="fas fa-comment-sms text-info me-2"></i> VoIP.ms SMS Integration</h5>
|
||||||
<div class="card-header bg-white pt-4 pb-3 border-bottom">
|
<p class="text-muted small">Configure VoIP.ms API credentials for delivering SMS 2FA codes.</p>
|
||||||
<h5 class="mb-0 fw-bold"><i class="fas fa-comment-sms text-info me-2"></i> VoIP.ms SMS Integration</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body p-4">
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label class="form-label fw-semibold">API Username</label>
|
<label class="form-label fw-semibold">API Username</label>
|
||||||
@@ -495,16 +462,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="messaging-plugins-list"></div>
|
<div id="messaging-plugins-list"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Proxy Secrets Pane -->
|
<!-- Proxy Secrets Tab -->
|
||||||
<div class="tab-pane fade" id="pane-proxy" role="tabpanel">
|
<div class="tab-pane fade" id="pane-proxy" role="tabpanel">
|
||||||
<div class="card shadow-sm border-0 mb-4">
|
<h5 class="fw-bold mb-3"><i class="fas fa-shield-alt text-warning me-2"></i> OpenBao Proxy Integration</h5>
|
||||||
<div class="card-header bg-white pt-4 pb-3 border-bottom">
|
|
||||||
<h5 class="mb-0 fw-bold"><i class="fas fa-shield-alt text-warning me-2"></i> OpenBao Proxy Integration</h5>
|
|
||||||
</div>
|
|
||||||
<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>
|
<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>
|
<h6 class="fw-bold text-dark mt-3 mb-2">OAuth / OIDC Client</h6>
|
||||||
@@ -537,17 +498,13 @@
|
|||||||
|
|
||||||
<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>
|
<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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Terms of Service Pane -->
|
<!-- Terms of Service Tab -->
|
||||||
<div class="tab-pane fade" id="pane-tos" role="tabpanel">
|
<div class="tab-pane fade" id="pane-tos" role="tabpanel">
|
||||||
<div class="card shadow-sm border-0 mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<div class="card-header bg-white pt-4 pb-3 border-bottom d-flex justify-content-between align-items-center">
|
<h5 class="fw-bold mb-0"><i class="fas fa-file-contract me-2"></i> Terms of Service Editor</h5>
|
||||||
<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>
|
||||||
<span class="small text-muted" id="tos-meta"></span>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="card-body p-4">
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label fw-semibold">Terms Content (Markdown)</label>
|
<label class="form-label fw-semibold">Terms Content (Markdown)</label>
|
||||||
<textarea class="form-control font-monospace" id="tos-content" rows="10" placeholder="Enter Terms of Service markdown content..."></textarea>
|
<textarea class="form-control font-monospace" id="tos-content" rows="10" placeholder="Enter Terms of Service markdown content..."></textarea>
|
||||||
@@ -559,9 +516,9 @@
|
|||||||
<button class="btn btn-primary" onclick="saveTos()"><i class="fas fa-floppy-disk me-1"></i> Save Terms of Service</button>
|
<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 id="tos-result" style="display:none" class="mt-3"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+76
-10
@@ -201,7 +201,10 @@
|
|||||||
<h5 class="fw-bold mb-1"><i class="fa-solid fa-plug text-primary me-2"></i> Discovery Plugins</h5>
|
<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>
|
<p class="text-muted small mb-0">Manage background discovery agents (Nmap, Docker, Proxmox, UniFi). Per-instance secrets are stored in OpenBao.</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-sm btn-outline-primary" onclick="loadDiscoveryPlugins()"><i class="fas fa-rotate me-1"></i> Refresh</button>
|
<div>
|
||||||
|
<button class="btn btn-sm btn-outline-primary me-2" onclick="loadDiscoveryPlugins()"><i class="fas fa-rotate me-1"></i> Refresh</button>
|
||||||
|
<button class="btn btn-sm btn-primary shadow-sm" onclick="openNewDiscoveryPluginModal()"><i class="fas fa-plus me-1"></i> New Plugin</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="discovery-plugins-list" class="mt-3"></div>
|
<div id="discovery-plugins-list" class="mt-3"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1259,9 +1262,10 @@
|
|||||||
function renderDiscoveryTable() {
|
function renderDiscoveryTable() {
|
||||||
const search = $('#discovery-search-filter').val().toLowerCase();
|
const search = $('#discovery-search-filter').val().toLowerCase();
|
||||||
const filtered = allDiscoveryResources.filter(r => {
|
const filtered = allDiscoveryResources.filter(r => {
|
||||||
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;
|
||||||
const isManaged = !!(r.metadata && r.metadata.managed);
|
// Directory contains managed items; Discovered Inventory only shows unmanaged/pending items awaiting promotion
|
||||||
if(isManaged) return false;
|
const isExplicitManaged = r.metadata && (r.metadata.managed === true || r.metadata.managed === 'true');
|
||||||
|
if (isExplicitManaged || r.kind === 'site' || r.kind === 'service') return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1582,15 +1586,77 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteDiscoveryPlugin(id) {
|
var discoveryPluginTypes = [];
|
||||||
const ok = await app.messages.confirm('Are you sure you want to delete this discovery plugin?');
|
|
||||||
if (!ok) return;
|
function openNewDiscoveryPluginModal() {
|
||||||
|
app.api.get('plugins/types', function(err, res) {
|
||||||
|
if (err) { app.messages.toast('Error loading plugin types: ' + err.message, 'danger'); return; }
|
||||||
|
discoveryPluginTypes = (res.results || []).filter(t => t.category === 'discovery');
|
||||||
|
if (discoveryPluginTypes.length === 0) {
|
||||||
|
app.messages.toast('No discovery plugin types available', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = discoveryPluginTypes.map(t => `<option value="${t.type}">${t.name} (${t.type})</option>`).join('');
|
||||||
|
const bodyHtml = `
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-bold">Plugin Type</label>
|
||||||
|
<select id="new-plugin-type" class="form-select shadow-sm">${options}</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-bold">Instance Name</label>
|
||||||
|
<input type="text" id="new-plugin-name" class="form-control shadow-sm" placeholder="e.g. Local Subnet Scanner">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-bold">Slug</label>
|
||||||
|
<input type="text" id="new-plugin-slug" class="form-control shadow-sm font-monospace" placeholder="e.g. local-subnet-scanner">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-bold">Cron Schedule</label>
|
||||||
|
<input type="text" id="new-plugin-cron" class="form-control shadow-sm font-monospace" value="*/15 * * * *">
|
||||||
|
<div class="form-text">Standard 5-field cron expression (e.g. */15 * * * * for every 15 mins)</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-check mb-3">
|
||||||
|
<input class="form-check-input" type="checkbox" id="new-plugin-enabled" checked>
|
||||||
|
<label class="form-check-label fw-semibold" for="new-plugin-enabled">Enable (load on create)</label>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-end gap-2">
|
||||||
|
<button class="btn btn-secondary" onclick="app.modal.close()">Cancel</button>
|
||||||
|
<button class="btn btn-primary" onclick="saveNewDiscoveryPlugin()">Create Plugin</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
app.modal.open({
|
||||||
|
title: 'Configure New Discovery Plugin',
|
||||||
|
bodyHtml: bodyHtml,
|
||||||
|
size: 'md'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveNewDiscoveryPlugin() {
|
||||||
|
const type = $('#new-plugin-type').val();
|
||||||
|
const name = $('#new-plugin-name').val().trim();
|
||||||
|
const slug = $('#new-plugin-slug').val().trim() || name.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
||||||
|
const cron = $('#new-plugin-cron').val().trim() || '*/15 * * * *';
|
||||||
|
const enabled = $('#new-plugin-enabled').is(':checked');
|
||||||
|
|
||||||
|
if (!name) return app.messages.action('Name is required', app.modal.body(), 'danger');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await app.api.delete(`plugins/${id}`);
|
await app.api.post('plugins', {
|
||||||
app.messages.toast('Discovery plugin deleted', 'success');
|
pluginType: type,
|
||||||
|
name,
|
||||||
|
slug,
|
||||||
|
cron,
|
||||||
|
enabled,
|
||||||
|
config: {}
|
||||||
|
});
|
||||||
|
app.messages.toast('Discovery plugin created successfully!', 'success');
|
||||||
|
app.modal.close();
|
||||||
loadDiscoveryPlugins();
|
loadDiscoveryPlugins();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
app.messages.toast('Error deleting plugin: ' + e.message, 'danger');
|
app.messages.action('Error creating plugin: ' + e.message, app.modal.body(), 'danger');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -134,7 +134,12 @@ curl "$VAULT_ADDR/v1/secret/data/apps/<span id="app-name-display"></span>/conf"
|
|||||||
// key relative to the subject's namespace (so 'foo' for a user means
|
// key relative to the subject's namespace (so 'foo' for a user means
|
||||||
// secret/data/users/<uid>/foo).
|
// secret/data/users/<uid>/foo).
|
||||||
function vpath(kind, key) {
|
function vpath(kind, key) {
|
||||||
return `secret/${kind}/${VAULT_BASE}${key}`;
|
let cleanKey = key || '';
|
||||||
|
if (cleanKey.startsWith('/')) cleanKey = cleanKey.slice(1);
|
||||||
|
if (VAULT_BASE) {
|
||||||
|
return `secret/${kind}/${VAULT_BASE}${cleanKey}`;
|
||||||
|
}
|
||||||
|
return `secret/${kind}/${cleanKey}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function apiCall(method, path, body = null) {
|
function apiCall(method, path, body = null) {
|
||||||
@@ -156,7 +161,8 @@ curl "$VAULT_ADDR/v1/secret/data/apps/<span id="app-name-display"></span>/conf"
|
|||||||
|
|
||||||
async function loadSecrets() {
|
async function loadSecrets() {
|
||||||
try {
|
try {
|
||||||
const res = await apiCall('GET', vpath('metadata', '?list=true'));
|
const listPath = vpath('metadata', '').replace(/\/$/, '') + '?list=true';
|
||||||
|
const res = await apiCall('GET', listPath);
|
||||||
const listEl = document.getElementById('secrets-list');
|
const listEl = document.getElementById('secrets-list');
|
||||||
listEl.innerHTML = '';
|
listEl.innerHTML = '';
|
||||||
if (!res || !res.data || !res.data.keys || res.data.keys.length === 0) {
|
if (!res || !res.data || !res.data.keys || res.data.keys.length === 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user