225 lines
9.0 KiB
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>{{formatTime .CreatedAt}}</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>{{formatTime .CreatedAt}}</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}}
|