feat: AMPP configuration in admin settings
- GET/PUT /api/ampp/config — store base URL + API key in db.json (no restart needed) - POST /api/ampp/test — authenticate against AMPP token endpoint and report result - Refactored AMPP_BASE/AMPP_API_KEY constants to dynamic getAmppBase()/getAmppApiKey() functions so live config changes take effect immediately - Admin → AMPP tab: base URL field, masked API key field, Test Connection + Save buttons - Cached token invalidated automatically on config save Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6b6efc61ba
commit
155c821ef8
2 changed files with 119 additions and 10 deletions
|
|
@ -519,6 +519,7 @@ body:not([data-theme="light"]) .header-wordmark{filter:invert(1)}
|
|||
<div class="admin-tabs">
|
||||
<div class="admin-tab active" onclick="switchAdminTab('s3')">S3 Storage</div>
|
||||
<div class="admin-tab" onclick="switchAdminTab('relay')">UDP Relay</div>
|
||||
<div class="admin-tab" onclick="switchAdminTab('ampp')">AMPP</div>
|
||||
<div class="admin-tab" onclick="switchAdminTab('users')">Users</div>
|
||||
<div class="admin-tab" onclick="switchAdminTab('folders')">Folders</div>
|
||||
</div>
|
||||
|
|
@ -557,6 +558,26 @@ body:not([data-theme="light"]) .header-wordmark{filter:invert(1)}
|
|||
<div class="status-msg" id="relay-status"></div>
|
||||
</div>
|
||||
|
||||
<!-- AMPP -->
|
||||
<div class="admin-panel" id="admin-ampp">
|
||||
<div class="section-title">AMPP Configuration</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Base URL</label>
|
||||
<input class="form-input" id="ampp-base-url" type="url" placeholder="https://us-east-1.gvampp.com"/>
|
||||
<div class="form-hint">Your Grass Valley AMPP regional endpoint</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">API Key (Base64 credentials)</label>
|
||||
<input class="form-input" id="ampp-api-key" type="password" placeholder="Leave blank to keep existing" autocomplete="new-password"/>
|
||||
<div class="form-hint" id="ampp-key-hint"></div>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn-secondary" onclick="testAmpp()">🔍 Test Connection</button>
|
||||
<button class="btn-primary" onclick="saveAmpp()">💾 Save Configuration</button>
|
||||
</div>
|
||||
<div class="status-msg" id="ampp-status"></div>
|
||||
</div>
|
||||
|
||||
<!-- Users -->
|
||||
<div class="admin-panel" id="admin-users">
|
||||
<div class="section-title">User Management</div>
|
||||
|
|
@ -692,7 +713,7 @@ function showApp() {
|
|||
document.getElementById('header-user').textContent = currentUser;
|
||||
if (currentRole === 'admin') {
|
||||
document.querySelectorAll('.admin-only').forEach(e => e.classList.remove('hidden'));
|
||||
loadS3Config(); loadRelayConfig(); loadUsers();
|
||||
loadS3Config(); loadRelayConfig(); loadAmppConfig(); loadUsers();
|
||||
}
|
||||
loadFolders();
|
||||
loadAmppJobs();
|
||||
|
|
@ -732,11 +753,12 @@ function switchPage(name) {
|
|||
|
||||
function switchAdminTab(name) {
|
||||
document.querySelectorAll('.admin-tab').forEach((t,i) => {
|
||||
t.classList.toggle('active', ['s3','relay','users','folders'][i] === name);
|
||||
t.classList.toggle('active', ['s3','relay','ampp','users','folders'][i] === name);
|
||||
});
|
||||
document.querySelectorAll('.admin-panel').forEach(p => p.classList.toggle('active', p.id === `admin-${name}`));
|
||||
if (name === 'users') loadUsers();
|
||||
if (name === 'folders') loadAdminFolders();
|
||||
if (name === 'ampp') loadAmppConfig();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
|
@ -1037,6 +1059,41 @@ async function saveRelay() {
|
|||
}
|
||||
function setRelayDot(c,t){document.getElementById('relay-dot').className=`relay-dot ${c}`;document.getElementById('relay-status-text').textContent=t;}
|
||||
|
||||
// ============================================================
|
||||
// AMPP CONFIG ADMIN
|
||||
// ============================================================
|
||||
async function loadAmppConfig() {
|
||||
try {
|
||||
const d=await api('GET','/api/ampp/config'); if(!d.success)return;
|
||||
document.getElementById('ampp-base-url').value=d.config.baseUrl||'';
|
||||
const hint=document.getElementById('ampp-key-hint');
|
||||
hint.textContent=d.config.apiKeyExists?'API key is saved (leave blank to keep)':'No API key saved yet';
|
||||
} catch(_){}
|
||||
}
|
||||
async function testAmpp() {
|
||||
const s=document.getElementById('ampp-status');
|
||||
s.className='status-msg loading'; s.textContent='🔍 Testing AMPP connection…';
|
||||
try {
|
||||
const body={baseUrl:document.getElementById('ampp-base-url').value.trim()};
|
||||
const k=document.getElementById('ampp-api-key').value.trim();
|
||||
if(k) body.apiKey=k;
|
||||
const d=await api('POST','/api/ampp/test',body);
|
||||
s.className=`status-msg ${d.success?'success':'error'}`; s.textContent=d.success?`✅ ${d.message}`:`❌ ${d.error}`;
|
||||
} catch(e){s.className='status-msg error';s.textContent=`❌ ${e.message}`;}
|
||||
}
|
||||
async function saveAmpp() {
|
||||
const s=document.getElementById('ampp-status');
|
||||
s.className='status-msg loading'; s.textContent='💾 Saving…';
|
||||
try {
|
||||
const body={baseUrl:document.getElementById('ampp-base-url').value.trim()};
|
||||
const k=document.getElementById('ampp-api-key').value.trim();
|
||||
if(k) body.apiKey=k;
|
||||
const d=await api('PUT','/api/ampp/config',body);
|
||||
s.className=`status-msg ${d.success?'success':'error'}`; s.textContent=d.success?'✅ AMPP configuration saved':`❌ ${d.error}`;
|
||||
if(d.success){document.getElementById('ampp-api-key').value='';loadAmppConfig();}
|
||||
} catch(e){s.className='status-msg error';s.textContent=`❌ ${e.message}`;}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// USERS ADMIN
|
||||
// ============================================================
|
||||
|
|
|
|||
68
server.js
68
server.js
|
|
@ -533,16 +533,18 @@ app.get("/api/udp/relay/health", async (req, res) => {
|
|||
});
|
||||
|
||||
// ==================== AMPP JOB MONITORING ====================
|
||||
const AMPP_BASE = process.env.AMPP_BASE_URL || "https://us-east-1.gvampp.com";
|
||||
const AMPP_API_KEY = process.env.AMPP_API_KEY || "";
|
||||
const AMPP_BASE_DEFAULT = process.env.AMPP_BASE_URL || "https://us-east-1.gvampp.com";
|
||||
function getAmppBase() { return (db.amppConfig?.baseUrl || AMPP_BASE_DEFAULT).trim(); }
|
||||
function getAmppApiKey() { return db.amppConfig?.apiKey || process.env.AMPP_API_KEY || ""; }
|
||||
|
||||
let amppToken = null;
|
||||
let amppTokenExpiry = 0;
|
||||
|
||||
async function getAmppToken() {
|
||||
if (amppToken && Date.now() < amppTokenExpiry - 60000) return amppToken;
|
||||
const r = await fetch(`${AMPP_BASE}/identity/connect/token`, {
|
||||
const r = await fetch(`${getAmppBase()}/identity/connect/token`, {
|
||||
method: "POST",
|
||||
headers: { "Authorization": `Basic ${AMPP_API_KEY}`, "Content-Type": "application/x-www-form-urlencoded" },
|
||||
headers: { "Authorization": `Basic ${getAmppApiKey()}`, "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: "grant_type=client_credentials&scope=platform",
|
||||
});
|
||||
if (!r.ok) throw new Error(`AMPP auth failed (${r.status})`);
|
||||
|
|
@ -552,13 +554,63 @@ async function getAmppToken() {
|
|||
return amppToken;
|
||||
}
|
||||
|
||||
// ---- AMPP Config admin endpoints ----
|
||||
app.get("/api/ampp/config", requireAdmin, (req, res) => {
|
||||
const cfg = db.amppConfig || {};
|
||||
res.json({
|
||||
success: true,
|
||||
config: {
|
||||
baseUrl: cfg.baseUrl || AMPP_BASE_DEFAULT,
|
||||
apiKeyExists: !!(cfg.apiKey),
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.put("/api/ampp/config", requireAdmin, (req, res) => {
|
||||
const { baseUrl, apiKey } = req.body;
|
||||
if (!db.amppConfig) db.amppConfig = {};
|
||||
if (baseUrl !== undefined) db.amppConfig.baseUrl = baseUrl.trim();
|
||||
if (apiKey) db.amppConfig.apiKey = apiKey.trim();
|
||||
amppToken = null; amppTokenExpiry = 0; // invalidate cached token
|
||||
saveData(db);
|
||||
res.json({ success: true, message: "AMPP configuration saved" });
|
||||
});
|
||||
|
||||
app.post("/api/ampp/test", requireAdmin, async (req, res) => {
|
||||
const { baseUrl, apiKey } = req.body;
|
||||
const testBase = (baseUrl || getAmppBase()).trim();
|
||||
const testApiKey = (apiKey || getAmppApiKey()).trim();
|
||||
if (!testApiKey) return res.status(400).json({ success: false, error: "API key is required to test" });
|
||||
try {
|
||||
const r = await fetch(`${testBase}/identity/connect/token`, {
|
||||
method: "POST",
|
||||
headers: { "Authorization": `Basic ${testApiKey}`, "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: "grant_type=client_credentials&scope=platform",
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
if (!r.ok) {
|
||||
const text = await r.text().catch(() => "");
|
||||
return res.status(400).json({ success: false, error: `Authentication failed (HTTP ${r.status})${text ? ': ' + text.slice(0, 120) : ''}` });
|
||||
}
|
||||
const data = await r.json();
|
||||
if (!data.access_token) return res.status(400).json({ success: false, error: "No access token returned — check credentials" });
|
||||
res.json({ success: true, message: "AMPP connection confirmed! Authentication successful." });
|
||||
} catch (err) {
|
||||
let msg = err.message;
|
||||
if (err.name === "TimeoutError" || err.name === "AbortError") msg = "Request timed out — check the base URL";
|
||||
else if (msg.includes("ECONNREFUSED")) msg = "Connection refused — check the base URL and network";
|
||||
else if (msg.includes("ENOTFOUND")) msg = "Host not found — check the base URL";
|
||||
res.status(400).json({ success: false, error: msg });
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/ampp/jobs", requireAuth, async (req, res) => {
|
||||
if (!AMPP_API_KEY) return res.status(503).json({ success: false, error: "AMPP API key not configured" });
|
||||
if (!getAmppApiKey()) return res.status(503).json({ success: false, error: "AMPP API key not configured" });
|
||||
try {
|
||||
const token = await getAmppToken();
|
||||
const limit = parseInt(req.query.limit) || 100;
|
||||
const skip = parseInt(req.query.skip) || 0;
|
||||
const url = `${AMPP_BASE}/api/v1/queue/job/jobs/querypage?skip=${skip}&limit=${limit}&sort=created:dateTime&asc=false`;
|
||||
const url = `${getAmppBase()}/api/v1/queue/job/jobs/querypage?skip=${skip}&limit=${limit}&sort=created:dateTime&asc=false`;
|
||||
const controller = new AbortController();
|
||||
const t = setTimeout(() => controller.abort(), 15000);
|
||||
const r = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` }, body: "{}", signal: controller.signal });
|
||||
|
|
@ -579,10 +631,10 @@ app.get("/api/ampp/jobs", requireAuth, async (req, res) => {
|
|||
});
|
||||
|
||||
app.get("/api/ampp/jobs/:jobId", requireAuth, async (req, res) => {
|
||||
if (!AMPP_API_KEY) return res.status(503).json({ success: false, error: "AMPP API key not configured" });
|
||||
if (!getAmppApiKey()) return res.status(503).json({ success: false, error: "AMPP API key not configured" });
|
||||
try {
|
||||
const token = await getAmppToken();
|
||||
const r = await fetch(`${AMPP_BASE}/api/v1/queue/job/jobs/${encodeURIComponent(req.params.jobId)}`, { headers: { "Authorization": `Bearer ${token}` } });
|
||||
const r = await fetch(`${getAmppBase()}/api/v1/queue/job/jobs/${encodeURIComponent(req.params.jobId)}`, { headers: { "Authorization": `Bearer ${token}` } });
|
||||
if (!r.ok) return res.status(r.status).json({ success: false, error: `AMPP ${r.status}` });
|
||||
res.json({ success: true, job: await r.json() });
|
||||
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
|
||||
|
|
|
|||
Loading…
Reference in a new issue