Files
keywarden/web/templates/keys.html

225 lines
9.0 KiB
HTML

{{define "content"}}
<div class="row row-cards">
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title"><i class="ti ti-key"></i> SSH Keys</h3>
<div class="card-actions">
<a href="/keys/generate" class="btn btn-primary">
<i class="ti ti-plus"></i> Generate New Key
</a>
<a href="/keys/import" class="btn btn-outline-primary ms-2">
<i class="ti ti-upload"></i> Import Key
</a>
</div>
</div>
<div class="table-responsive">
<table class="table table-vcenter card-table">
<thead>
<tr>
{{if or (eq .User.Role "admin") (eq .User.Role "owner")}}
<th>Owner</th>
{{end}}
<th>Name</th>
<th>Type</th>
<th>Bits</th>
<th>Fingerprint</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{{if or (eq $.User.Role "admin") (eq $.User.Role "owner")}}
{{/* Admin/Owner view: show all keys with owner info */}}
{{range .Data}}
<tr>
<td>
<span class="badge bg-{{if eq .OwnerUsername $.User.Username}}green-lt{{else}}blue-lt{{end}}">{{.OwnerUsername}}</span>
</td>
<td>
<div class="d-flex align-items-center">
<i class="ti ti-key me-2 text-primary"></i>
<strong>{{.Name}}</strong>
</div>
</td>
<td>
<span class="badge bg-{{if eq .KeyType "ed25519"}}green-lt{{else}}azure-lt{{end}}">{{.KeyType}}</span>
</td>
<td>{{.Bits}}</td>
<td><code class="small">{{.Fingerprint}}</code></td>
<td>{{.CreatedAt.Format "2006-01-02 15:04"}}</td>
<td>
<div class="btn-list flex-nowrap">
<button type="button" class="btn btn-sm btn-icon btn-outline-primary" title="View Public Key" onclick="showPublicKey({{.ID}}, '{{.Name}}')">
<i class="ti ti-eye"></i>
</button>
{{if eq .UserID $.User.ID}}
<a href="/keys/{{.ID}}/download" class="btn btn-sm btn-icon btn-outline-secondary" title="Download Private Key">
<i class="ti ti-download"></i>
</a>
{{end}}
<form method="POST" action="/keys/{{.ID}}/delete" class="d-inline" onsubmit="return confirm('Delete this key?')">
<button type="submit" class="btn btn-sm btn-icon btn-outline-danger" title="Delete">
<i class="ti ti-trash"></i>
</button>
</form>
</div>
</td>
</tr>
{{else}}
<tr>
<td colspan="7" class="text-center text-secondary py-4">
<i class="ti ti-key-off" style="font-size: 2rem;"></i>
<p class="mt-2">No SSH keys found. Generate or import one to get started.</p>
</td>
</tr>
{{end}}
{{else}}
{{/* User view: show only own keys */}}
{{range .Keys}}
<tr>
<td>
<div class="d-flex align-items-center">
<i class="ti ti-key me-2 text-primary"></i>
<strong>{{.Name}}</strong>
</div>
</td>
<td>
<span class="badge bg-{{if eq .KeyType "ed25519"}}green-lt{{else}}azure-lt{{end}}">{{.KeyType}}</span>
</td>
<td>{{.Bits}}</td>
<td><code class="small">{{.Fingerprint}}</code></td>
<td>{{.CreatedAt.Format "2006-01-02 15:04"}}</td>
<td>
<div class="btn-list flex-nowrap">
<button type="button" class="btn btn-sm btn-icon btn-outline-primary" title="View Public Key" onclick="showPublicKey({{.ID}}, '{{.Name}}')">
<i class="ti ti-eye"></i>
</button>
<a href="/keys/{{.ID}}/download" class="btn btn-sm btn-icon btn-outline-secondary" title="Download Private Key">
<i class="ti ti-download"></i>
</a>
<form method="POST" action="/keys/{{.ID}}/delete" class="d-inline" onsubmit="return confirm('Delete this key?')">
<button type="submit" class="btn btn-sm btn-icon btn-outline-danger" title="Delete">
<i class="ti ti-trash"></i>
</button>
</form>
</div>
</td>
</tr>
{{else}}
<tr>
<td colspan="6" class="text-center text-secondary py-4">
<i class="ti ti-key-off" style="font-size: 2rem;"></i>
<p class="mt-2">No SSH keys found. Generate or import one to get started.</p>
</td>
</tr>
{{end}}
{{end}}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Public Key Modal -->
<div class="modal modal-blur fade" id="publicKeyModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="ti ti-key text-primary"></i> Public Key: <span id="publicKeyName"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="publicKeyLoading" class="text-center py-4">
<div class="spinner-border text-primary" role="status"></div>
<p class="mt-2 text-secondary">Loading public key...</p>
</div>
<div id="publicKeyContent" class="d-none">
<textarea id="publicKeyText" class="form-control" rows="6" readonly style="font-family: monospace; font-size: 0.85rem;"></textarea>
</div>
<div id="publicKeyError" class="d-none">
<div class="alert alert-danger mb-0">
<i class="ti ti-alert-triangle"></i> Failed to load public key.
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="copyPublicKeyBtn" onclick="copyPublicKey()">
<i class="ti ti-copy"></i> Copy to Clipboard
</button>
</div>
</div>
</div>
</div>
<script>
function showPublicKey(keyID, keyName) {
document.getElementById('publicKeyName').textContent = keyName;
document.getElementById('publicKeyLoading').classList.remove('d-none');
document.getElementById('publicKeyContent').classList.add('d-none');
document.getElementById('publicKeyError').classList.add('d-none');
document.getElementById('copyPublicKeyBtn').classList.remove('d-none');
var modal = new bootstrap.Modal(document.getElementById('publicKeyModal'));
modal.show();
fetch('/keys/' + keyID + '/view')
.then(function(response) {
if (!response.ok) throw new Error('Failed to load key');
return response.text();
})
.then(function(pubKey) {
document.getElementById('publicKeyText').value = pubKey;
document.getElementById('publicKeyLoading').classList.add('d-none');
document.getElementById('publicKeyContent').classList.remove('d-none');
})
.catch(function() {
document.getElementById('publicKeyLoading').classList.add('d-none');
document.getElementById('publicKeyError').classList.remove('d-none');
document.getElementById('copyPublicKeyBtn').classList.add('d-none');
});
}
function copyPublicKey() {
var textarea = document.getElementById('publicKeyText');
var text = textarea.value;
var success = false;
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).then(onCopySuccess).catch(fallbackCopy);
return;
}
fallbackCopy();
function fallbackCopy() {
textarea.select();
textarea.setSelectionRange(0, 99999);
try { success = document.execCommand('copy'); } catch(e) { success = false; }
if (success) { onCopySuccess(); } else { alert('Copy failed. Please select the key manually and copy it.'); }
}
function onCopySuccess() {
var btn = document.getElementById('copyPublicKeyBtn');
btn.innerHTML = '<i class="ti ti-check"></i> Copied!';
btn.classList.remove('btn-primary');
btn.classList.add('btn-success');
setTimeout(function() {
btn.innerHTML = '<i class="ti ti-copy"></i> Copy to Clipboard';
btn.classList.remove('btn-success');
btn.classList.add('btn-primary');
}, 2000);
}
}
// Move modal to body so it is not clipped by overflow containers
document.addEventListener('DOMContentLoaded', function() {
var modal = document.getElementById('publicKeyModal');
if (modal) {
document.body.appendChild(modal);
}
});
</script>
{{end}}