feat(release): v1.14.0 discovery and conf pages
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
<%- include('top') %>
|
||||
|
||||
<div class="container-fluid py-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="fas fa-lock"></i> Vault Secrets</h2>
|
||||
<button class="btn btn-primary" onclick="showCreateModal()">
|
||||
<i class="fas fa-plus"></i> New Secret
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="card-title mb-0">Secrets List</h5>
|
||||
</div>
|
||||
<div class="list-group list-group-flush" id="secrets-list">
|
||||
<div class="list-group-item text-center text-muted">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow-sm" id="secret-details-card" style="display: none;">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0" id="secret-title">Secret Details</h5>
|
||||
<div>
|
||||
<button class="btn btn-sm btn-outline-primary me-2" onclick="editCurrentSecret()">
|
||||
<i class="fas fa-edit"></i> Edit
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="deleteCurrentSecret()">
|
||||
<i class="fas fa-trash"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<pre id="secret-content" class="bg-dark text-light p-3 rounded" style="min-height: 200px;"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="no-secret-selected" class="text-center text-muted mt-5">
|
||||
<i class="fas fa-key fa-4x mb-3 text-secondary"></i>
|
||||
<h4>Select a secret to view its details</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create/Edit Secret Modal -->
|
||||
<div class="modal fade" id="secretModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="secretModalTitle">Create Secret</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Secret Path (Name)</label>
|
||||
<input type="text" class="form-control" id="secret-path-input" placeholder="e.g. database-creds">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Secret Data (JSON)</label>
|
||||
<textarea class="form-control" id="secret-data-input" rows="8" style="font-family: monospace;">{
|
||||
"username": "",
|
||||
"password": ""
|
||||
}</textarea>
|
||||
</div>
|
||||
<div class="alert alert-danger d-none" id="secret-error"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveSecret()">Save Secret</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
app.auth.forceLogin();
|
||||
|
||||
const VAULT_TOKEN = 'root'; // Dev mode root token
|
||||
let currentSecretPath = null;
|
||||
const secretModal = new bootstrap.Modal(document.getElementById('secretModal'));
|
||||
|
||||
function apiCall(method, path, body = null) {
|
||||
const opts = {
|
||||
method,
|
||||
headers: {
|
||||
'X-Vault-Token': VAULT_TOKEN,
|
||||
'Content-Type': 'application/json',
|
||||
'auth-token': app.auth.getToken()
|
||||
}
|
||||
};
|
||||
if (body) opts.body = JSON.stringify(body);
|
||||
return fetch('/api/vault/' + path, opts).then(async res => {
|
||||
if (res.status === 404) return null;
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(`Vault API error: ${res.status} ${text}`);
|
||||
}
|
||||
if (res.status === 204) return null;
|
||||
return res.json();
|
||||
});
|
||||
}
|
||||
|
||||
async function loadSecrets() {
|
||||
try {
|
||||
// In dev mode, v2 kv engine is mounted at secret/
|
||||
const res = await apiCall('GET', 'secret/metadata/?list=true');
|
||||
const listEl = document.getElementById('secrets-list');
|
||||
listEl.innerHTML = '';
|
||||
|
||||
if (!res || !res.data || !res.data.keys || res.data.keys.length === 0) {
|
||||
listEl.innerHTML = '<div class="list-group-item text-center text-muted">No secrets found</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
res.data.keys.forEach(key => {
|
||||
const item = document.createElement('a');
|
||||
item.href = '#';
|
||||
item.className = 'list-group-item list-group-item-action d-flex align-items-center';
|
||||
item.innerHTML = `<i class="fas fa-file-alt text-secondary me-3"></i> <span>${key}</span>`;
|
||||
item.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Update active state
|
||||
document.querySelectorAll('#secrets-list .active').forEach(el => el.classList.remove('active'));
|
||||
item.classList.add('active');
|
||||
|
||||
loadSecretDetails(key);
|
||||
};
|
||||
listEl.appendChild(item);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
document.getElementById('secrets-list').innerHTML =
|
||||
`<div class="list-group-item text-danger"><i class="fas fa-exclamation-triangle"></i> Error loading secrets: ${err.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSecretDetails(key) {
|
||||
try {
|
||||
currentSecretPath = key;
|
||||
document.getElementById('no-secret-selected').style.display = 'none';
|
||||
document.getElementById('secret-details-card').style.display = 'block';
|
||||
document.getElementById('secret-title').textContent = key;
|
||||
document.getElementById('secret-content').textContent = 'Loading...';
|
||||
|
||||
const res = await apiCall('GET', `secret/data/${key}`);
|
||||
if (!res || !res.data || !res.data.data) {
|
||||
document.getElementById('secret-content').textContent = 'No data found.';
|
||||
} else {
|
||||
document.getElementById('secret-content').textContent = JSON.stringify(res.data.data, null, 2);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
document.getElementById('secret-content').textContent = `Error: ${err.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
function showCreateModal() {
|
||||
currentSecretPath = null;
|
||||
document.getElementById('secretModalTitle').textContent = 'Create Secret';
|
||||
document.getElementById('secret-path-input').value = '';
|
||||
document.getElementById('secret-path-input').disabled = false;
|
||||
document.getElementById('secret-data-input').value = '{\n "key": "value"\n}';
|
||||
document.getElementById('secret-error').classList.add('d-none');
|
||||
secretModal.show();
|
||||
}
|
||||
|
||||
function editCurrentSecret() {
|
||||
if (!currentSecretPath) return;
|
||||
document.getElementById('secretModalTitle').textContent = 'Edit Secret';
|
||||
document.getElementById('secret-path-input').value = currentSecretPath;
|
||||
document.getElementById('secret-path-input').disabled = true;
|
||||
document.getElementById('secret-data-input').value = document.getElementById('secret-content').textContent;
|
||||
document.getElementById('secret-error').classList.add('d-none');
|
||||
secretModal.show();
|
||||
}
|
||||
|
||||
async function saveSecret() {
|
||||
const errorEl = document.getElementById('secret-error');
|
||||
errorEl.classList.add('d-none');
|
||||
|
||||
const path = document.getElementById('secret-path-input').value.trim();
|
||||
if (!path) {
|
||||
errorEl.textContent = 'Secret path is required';
|
||||
errorEl.classList.remove('d-none');
|
||||
return;
|
||||
}
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(document.getElementById('secret-data-input').value);
|
||||
} catch (err) {
|
||||
errorEl.textContent = 'Invalid JSON: ' + err.message;
|
||||
errorEl.classList.remove('d-none');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiCall('POST', `secret/data/${path}`, { data });
|
||||
secretModal.hide();
|
||||
await loadSecrets();
|
||||
if (currentSecretPath === path || !currentSecretPath) {
|
||||
await loadSecretDetails(path);
|
||||
}
|
||||
} catch (err) {
|
||||
errorEl.textContent = err.message;
|
||||
errorEl.classList.remove('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCurrentSecret() {
|
||||
if (!currentSecretPath) return;
|
||||
if (!confirm(`Are you sure you want to delete the secret '${currentSecretPath}'?`)) return;
|
||||
|
||||
try {
|
||||
await apiCall('DELETE', `secret/metadata/${currentSecretPath}`);
|
||||
currentSecretPath = null;
|
||||
document.getElementById('no-secret-selected').style.display = 'block';
|
||||
document.getElementById('secret-details-card').style.display = 'none';
|
||||
await loadSecrets();
|
||||
} catch (err) {
|
||||
alert('Error deleting secret: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Init
|
||||
loadSecrets();
|
||||
</script>
|
||||
|
||||
<%- include('bottom') %>
|
||||
Reference in New Issue
Block a user