Initial commit: DashCaddy v1.0
Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
This commit is contained in:
284
status/js/service-credentials.js
Normal file
284
status/js/service-credentials.js
Normal file
@@ -0,0 +1,284 @@
|
||||
// ===== SERVICE CREDENTIALS =====
|
||||
(function() {
|
||||
injectModal('folder-browser-modal', `<div id="folder-browser-modal" class="weather-modal">
|
||||
<div class="weather-modal-content" style="min-width: 500px; max-width: 700px; max-height: 80vh;">
|
||||
<h3>📂 Browse for Media Folders</h3>
|
||||
|
||||
<div id="folder-browser-path" style="padding: 10px; background: var(--card-bg); border-radius: 6px; margin-bottom: 12px; font-family: monospace; font-size: 0.9rem; word-break: break-all;">
|
||||
/
|
||||
</div>
|
||||
|
||||
<div id="folder-browser-list" style="max-height: 400px; overflow-y: auto; border: 1px solid var(--border); border-radius: 6px;">
|
||||
<div style="padding: 20px; text-align: center; color: var(--muted);">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div id="folder-browser-selected" style="margin-top: 12px; padding: 10px; background: color-mix(in srgb, var(--success) 10%, transparent); border: 1px solid var(--success); border-radius: 6px; display: none;">
|
||||
<div style="font-size: 0.85rem; color: var(--success); margin-bottom: 6px;">Selected folders:</div>
|
||||
<div id="folder-browser-selected-list" style="display: flex; flex-wrap: wrap; gap: 6px;"></div>
|
||||
</div>
|
||||
|
||||
<div class="weather-modal-buttons" style="margin-top: 16px; display: flex; gap: 8px; justify-content: space-between;">
|
||||
<button id="folder-browser-select-current" class="btn-accent">
|
||||
+ Add Current Folder
|
||||
</button>
|
||||
<div class="flex-row-gap">
|
||||
<button id="folder-browser-cancel">Cancel</button>
|
||||
<button id="folder-browser-done" style="background: color-mix(in srgb, var(--success) 20%, transparent); border-color: var(--success); color: var(--success);">Done</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`);
|
||||
|
||||
injectModal('service-creds-modal', `<div id="service-creds-modal">
|
||||
<div class="service-creds-content">
|
||||
<h3 id="svc-creds-title" style="margin: 0 0 4px; font-size: 1.05rem;">Service Credentials</h3>
|
||||
<p id="svc-creds-desc" style="font-size: 0.75rem; color: var(--muted); margin: 0 0 14px;">Credentials are injected automatically when accessing this service.</p>
|
||||
|
||||
<!-- Status indicator -->
|
||||
<div style="display: flex; align-items: center; gap: 6px; margin-bottom: 12px;">
|
||||
<span id="svc-creds-dot" class="status-dot"></span>
|
||||
<span id="svc-creds-status" class="text-muted-sm">No credentials stored</span>
|
||||
</div>
|
||||
|
||||
<!-- Seedhost credentials (shown for external services) -->
|
||||
<div id="svc-creds-seedhost" style="display: none; margin-bottom: 14px;">
|
||||
<label class="label-bold">Seedhost Login</label>
|
||||
<p class="hint-micro">Username shared across all services. Password is per-service.</p>
|
||||
<input type="text" id="svc-seedhost-user" placeholder="Username (shared)" autocomplete="username"
|
||||
style="width: 100%; padding: 8px 10px; margin-bottom: 6px; background: var(--bg); color: var(--fg); border: 1px solid var(--border); border-radius: 6px; font-size: 0.85rem; box-sizing: border-box;" />
|
||||
<input type="password" id="svc-seedhost-pass" placeholder="Password" autocomplete="current-password"
|
||||
class="input-creds" />
|
||||
</div>
|
||||
|
||||
<!-- API Key (shown for arr services or services with API key support) -->
|
||||
<div id="svc-creds-apikey" style="display: none; margin-bottom: 14px;">
|
||||
<label class="label-bold">API Key</label>
|
||||
<p class="hint-micro">Bypasses the app's own login screen</p>
|
||||
<input type="text" id="svc-apikey-input" placeholder="API key"
|
||||
style="width: 100%; padding: 8px 10px; background: var(--bg); color: var(--fg); border: 1px solid var(--border); border-radius: 6px; font-size: 0.85rem; font-family: monospace; box-sizing: border-box;" />
|
||||
</div>
|
||||
|
||||
<!-- Per-service Basic Auth (shown for non-external services) -->
|
||||
<div id="svc-creds-basic" style="display: none; margin-bottom: 14px;">
|
||||
<label class="label-bold">Service Login</label>
|
||||
<input type="text" id="svc-basic-user" placeholder="Username" autocomplete="username"
|
||||
style="width: 100%; padding: 8px 10px; margin-top: 6px; margin-bottom: 6px; background: var(--bg); color: var(--fg); border: 1px solid var(--border); border-radius: 6px; font-size: 0.85rem; box-sizing: border-box;" />
|
||||
<input type="password" id="svc-basic-pass" placeholder="Password" autocomplete="current-password"
|
||||
class="input-creds" />
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div style="display: flex; gap: 8px; margin-top: 14px;">
|
||||
<button id="svc-creds-save" style="flex: 1; padding: 9px; background: var(--accent, #8FD6FF); color: #0b0f1a; border: none; border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 0.85rem;">
|
||||
Save
|
||||
</button>
|
||||
<button id="svc-creds-clear" style="padding: 9px 14px; background: transparent; color: var(--bad-fg, #ff9aa3); border: 1px solid var(--bad-fg, #ff9aa3); border-radius: 6px; cursor: pointer; font-size: 0.85rem; display: none;">
|
||||
Clear
|
||||
</button>
|
||||
<button id="svc-creds-close" style="padding: 9px 16px; background: var(--card-base); color: var(--fg); border: 1px solid var(--border); border-radius: 6px; cursor: pointer; font-size: 0.85rem;">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`);
|
||||
|
||||
const modal = document.getElementById('service-creds-modal');
|
||||
let currentService = null;
|
||||
const arrServices = ['sonarr', 'radarr', 'prowlarr', 'overseerr'];
|
||||
|
||||
window.openServiceCredsModal = async function(service) {
|
||||
currentService = service;
|
||||
const title = document.getElementById('svc-creds-title');
|
||||
const desc = document.getElementById('svc-creds-desc');
|
||||
const seedhostSection = document.getElementById('svc-creds-seedhost');
|
||||
const apikeySection = document.getElementById('svc-creds-apikey');
|
||||
const basicSection = document.getElementById('svc-creds-basic');
|
||||
|
||||
title.textContent = service.name + ' Credentials';
|
||||
// Determine which sections to show
|
||||
const isExt = !!service.isExternal;
|
||||
const isArr = arrServices.includes(service.id) || arrServices.includes(service.appTemplate);
|
||||
|
||||
seedhostSection.style.display = isExt ? '' : 'none';
|
||||
apikeySection.style.display = isArr ? '' : 'none';
|
||||
basicSection.style.display = !isExt ? '' : 'none';
|
||||
|
||||
if (isExt) {
|
||||
desc.textContent = 'Seedhost credentials auto-login past the HTTP prompt. API key bypasses the app login.';
|
||||
// Update password placeholder with service name
|
||||
document.getElementById('svc-seedhost-pass').placeholder = `Password for ${service.name}`;
|
||||
} else if (isArr) {
|
||||
desc.textContent = 'API key bypasses the app login screen automatically.';
|
||||
} else {
|
||||
desc.textContent = 'Credentials are injected automatically when accessing this service.';
|
||||
}
|
||||
|
||||
// Load existing credentials
|
||||
await loadServiceCreds(service);
|
||||
modal.classList.add('show');
|
||||
};
|
||||
|
||||
async function loadServiceCreds(service) {
|
||||
const dot = document.getElementById('svc-creds-dot');
|
||||
const status = document.getElementById('svc-creds-status');
|
||||
const clearBtn = document.getElementById('svc-creds-clear');
|
||||
let hasCreds = false;
|
||||
|
||||
// Load seedhost creds (shared username + per-service password)
|
||||
if (service.isExternal) {
|
||||
try {
|
||||
const res = await fetch(`/api/v1/seedhost-creds?serviceId=${service.id}`);
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
document.getElementById('svc-seedhost-user').value = data.username || '';
|
||||
if (data.hasCredentials) hasCreds = true;
|
||||
} else {
|
||||
document.getElementById('svc-seedhost-user').value = '';
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
document.getElementById('svc-seedhost-pass').value = '';
|
||||
}
|
||||
|
||||
// Load per-service creds
|
||||
try {
|
||||
const res = await fetch(`/api/v1/services/${service.id}/credentials`);
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
if (data.hasApiKey) {
|
||||
document.getElementById('svc-apikey-input').value = '••••••••';
|
||||
hasCreds = true;
|
||||
} else {
|
||||
document.getElementById('svc-apikey-input').value = '';
|
||||
}
|
||||
if (data.hasBasicAuth && !service.isExternal) {
|
||||
document.getElementById('svc-basic-user').value = data.username || '';
|
||||
hasCreds = true;
|
||||
} else {
|
||||
document.getElementById('svc-basic-user').value = '';
|
||||
}
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
if (document.getElementById('svc-basic-pass')) document.getElementById('svc-basic-pass').value = '';
|
||||
|
||||
if (hasCreds) {
|
||||
dot.style.background = 'var(--ok-fg, #74dfc4)';
|
||||
status.style.color = 'var(--ok-fg, #74dfc4)';
|
||||
status.textContent = 'Credentials stored';
|
||||
clearBtn.style.display = '';
|
||||
// Update the card button
|
||||
const btn = document.getElementById(`creds-btn-${service.id}`);
|
||||
if (btn) btn.classList.add('has-creds');
|
||||
} else {
|
||||
dot.style.background = 'var(--muted)';
|
||||
status.style.color = 'var(--muted)';
|
||||
status.textContent = 'No credentials stored';
|
||||
clearBtn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Save button
|
||||
document.getElementById('svc-creds-save')?.addEventListener('click', async () => {
|
||||
if (!currentService) return;
|
||||
const saveBtn = document.getElementById('svc-creds-save');
|
||||
saveBtn.textContent = 'Saving...';
|
||||
saveBtn.disabled = true;
|
||||
|
||||
try {
|
||||
// Save seedhost creds (shared username + per-service password)
|
||||
if (currentService.isExternal) {
|
||||
const user = document.getElementById('svc-seedhost-user').value.trim();
|
||||
const pass = document.getElementById('svc-seedhost-pass').value;
|
||||
if (user) {
|
||||
await secureFetch('/api/v1/seedhost-creds', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: user, password: pass || undefined, serviceId: currentService.id })
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Save API key
|
||||
const apiKeyInput = document.getElementById('svc-apikey-input');
|
||||
const apiKey = apiKeyInput.value.trim();
|
||||
if (apiKey && apiKey !== '••••••••') {
|
||||
await secureFetch(`/api/v1/services/${currentService.id}/credentials`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ apiKey })
|
||||
});
|
||||
}
|
||||
|
||||
// Save per-service basic auth
|
||||
if (!currentService.isExternal) {
|
||||
const user = document.getElementById('svc-basic-user').value.trim();
|
||||
const pass = document.getElementById('svc-basic-pass').value;
|
||||
if (user && pass) {
|
||||
await secureFetch(`/api/v1/services/${currentService.id}/credentials`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: user, password: pass })
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await loadServiceCreds(currentService);
|
||||
} catch (e) {
|
||||
console.error('Failed to save credentials:', e);
|
||||
}
|
||||
saveBtn.textContent = 'Save';
|
||||
saveBtn.disabled = false;
|
||||
});
|
||||
|
||||
// Clear button
|
||||
document.getElementById('svc-creds-clear')?.addEventListener('click', async () => {
|
||||
if (!currentService) return;
|
||||
if (!confirm(`Remove stored credentials for ${currentService.name}?`)) return;
|
||||
try {
|
||||
if (currentService.isExternal) {
|
||||
await secureFetch(`/api/v1/seedhost-creds?serviceId=${currentService.id}`, { method: 'DELETE' });
|
||||
}
|
||||
await secureFetch(`/api/v1/services/${currentService.id}/credentials`, { method: 'DELETE' });
|
||||
const btn = document.getElementById(`creds-btn-${currentService.id}`);
|
||||
if (btn) btn.classList.remove('has-creds');
|
||||
await loadServiceCreds(currentService);
|
||||
} catch (e) {
|
||||
console.error('Failed to clear credentials:', e);
|
||||
}
|
||||
});
|
||||
|
||||
// Close button / backdrop
|
||||
document.getElementById('svc-creds-close')?.addEventListener('click', () => {
|
||||
modal.classList.remove('show');
|
||||
currentService = null;
|
||||
});
|
||||
modal?.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
modal.classList.remove('show');
|
||||
currentService = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Check credential status for all services on page load (update key button highlights)
|
||||
window.refreshCredsButtons = async function() {
|
||||
try {
|
||||
for (const app of (window.APPS || [])) {
|
||||
if (!app.isExternal && !app.appTemplate && !app.url) continue;
|
||||
let hasCreds = false;
|
||||
if (app.isExternal) {
|
||||
try {
|
||||
const seedRes = await fetch(`/api/v1/seedhost-creds?serviceId=${app.id}`);
|
||||
const seedData = await seedRes.json();
|
||||
if (seedData.success && seedData.hasCredentials) hasCreds = true;
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
try {
|
||||
const r = await fetch(`/api/v1/services/${app.id}/credentials`);
|
||||
const d = await r.json();
|
||||
if (d.success && (d.hasApiKey || d.hasBasicAuth)) hasCreds = true;
|
||||
} catch (e) { /* ignore */ }
|
||||
const btn = document.getElementById(`creds-btn-${app.id}`);
|
||||
if (btn) btn.classList.toggle('has-creds', hasCreds);
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user