463 lines
18 KiB
HTML
463 lines
18 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>SigSocket Demo App</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
|
<style>
|
|
body {
|
|
font-family: Arial, sans-serif;
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
h1 {
|
|
color: #333;
|
|
text-align: center;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.container {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.panel {
|
|
flex: 1;
|
|
padding: 20px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 5px;
|
|
margin: 0 10px;
|
|
}
|
|
|
|
label {
|
|
display: block;
|
|
margin-bottom: 5px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
input[type="text"],
|
|
textarea {
|
|
width: 100%;
|
|
padding: 8px;
|
|
margin-bottom: 15px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
textarea {
|
|
min-height: 150px;
|
|
resize: vertical;
|
|
}
|
|
|
|
button {
|
|
background-color: #4CAF50;
|
|
color: white;
|
|
padding: 10px 15px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 16px;
|
|
}
|
|
|
|
button:hover {
|
|
background-color: #45a049;
|
|
}
|
|
|
|
.result {
|
|
background-color: #f9f9f9;
|
|
padding: 15px;
|
|
border-radius: 4px;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.success {
|
|
color: #4CAF50;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.error {
|
|
color: #f44336;
|
|
font-weight: bold;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>SigSocket Demo Application</h1>
|
|
|
|
<div class="container">
|
|
<!-- Left Panel - Message Input Form -->
|
|
<div class="panel">
|
|
<h2>Sign Message</h2>
|
|
<form action="/sign" method="post">
|
|
<div>
|
|
<label for="public_key">Public Key:</label>
|
|
<input type="text" id="public_key" name="public_key" placeholder="Enter the client's public key" required>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="message">Message to Sign:</label>
|
|
<textarea id="message" name="message" placeholder="Enter the message to be signed" required></textarea>
|
|
</div>
|
|
|
|
<button type="submit">Sign with SigSocket</button>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Right Panel - Signature Results -->
|
|
<div class="panel">
|
|
<h2>Pending Signatures</h2>
|
|
<div id="signature-list">
|
|
{% if has_requests %}
|
|
<div class="table-responsive">
|
|
<table class="table table-striped table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>Message</th>
|
|
<th>Status</th>
|
|
<th>Created</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for req in signature_requests %}
|
|
<tr id="signature-row-{{ req.id }}" class="{% if req.status == 'Success' %}table-success{% elif req.status == 'Error' or req.status == 'Timeout' %}table-danger{% elif req.status == 'Processing' %}table-warning{% else %}table-light{% endif %}">
|
|
<td>{{ req.id | truncate(length=8) }}</td>
|
|
<td>{{ req.message | truncate(length=20, end="...") }}</td>
|
|
<td>
|
|
<span class="badge rounded-pill {% if req.status == 'Success' %}bg-success{% elif req.status == 'Error' or req.status == 'Timeout' %}bg-danger{% elif req.status == 'Processing' %}bg-warning{% else %}bg-secondary{% endif %}">
|
|
{{ req.status }}
|
|
</span>
|
|
</td>
|
|
<td>{{ req.created_at }}</td>
|
|
<td>
|
|
<button class="btn btn-sm btn-info" onclick="viewSignature('{{ req.id }}')">
|
|
View
|
|
</button>
|
|
<button class="btn btn-sm btn-danger" onclick="deleteSignature('{{ req.id }}')">
|
|
Delete
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<p>No pending signatures. Submit a request using the form on the left.</p>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Signature details modal -->
|
|
<div class="modal fade" id="signatureDetailsModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Signature Details</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body" id="signature-details-content">
|
|
<!-- Content will be loaded dynamically -->
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="text-align: center; margin-top: 30px;">
|
|
<p>
|
|
This demo uses the SigSocket WebSocket-based signing service.
|
|
Make sure a SigSocket client is connected with the matching public key.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Toast container for notifications -->
|
|
<div class="toast-container position-fixed bottom-0 start-0 p-3" style="z-index: 11; width: 100%;">
|
|
<!-- Toasts will be added here dynamically -->
|
|
</div>
|
|
|
|
<script>
|
|
// Auto-refresh signature list every 2 seconds
|
|
let refreshTimer;
|
|
let signatureDetailsModal;
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Initialize the signature details modal
|
|
signatureDetailsModal = new bootstrap.Modal(document.getElementById('signatureDetailsModal'));
|
|
|
|
// Start auto-refresh
|
|
startAutoRefresh();
|
|
});
|
|
|
|
function startAutoRefresh() {
|
|
// Clear any existing timer
|
|
if (refreshTimer) {
|
|
clearInterval(refreshTimer);
|
|
}
|
|
|
|
// Setup timer to refresh signatures every 2 seconds
|
|
refreshTimer = setInterval(refreshSignatures, 2000);
|
|
console.log('Auto-refresh started');
|
|
}
|
|
|
|
function stopAutoRefresh() {
|
|
if (refreshTimer) {
|
|
clearInterval(refreshTimer);
|
|
refreshTimer = null;
|
|
console.log('Auto-refresh stopped');
|
|
}
|
|
}
|
|
|
|
function refreshSignatures() {
|
|
fetch('/api/signatures/all')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
updateSignatureTable(data.requests);
|
|
}
|
|
})
|
|
.catch(err => {
|
|
console.error('Error refreshing signatures: ' + err);
|
|
stopAutoRefresh(); // Stop on error
|
|
});
|
|
}
|
|
|
|
function updateSignatureTable(signatures) {
|
|
const tableBody = document.querySelector('#signature-list table tbody');
|
|
if (!tableBody && signatures.length > 0) {
|
|
// No table exists but we have signatures - reload the page
|
|
window.location.reload();
|
|
return;
|
|
} else if (!tableBody) {
|
|
return; // No table and no signatures - nothing to do
|
|
}
|
|
|
|
if (signatures.length === 0) {
|
|
document.getElementById('signature-list').innerHTML = '<p>No pending signatures. Submit a request using the form on the left.</p>';
|
|
return;
|
|
}
|
|
|
|
// Update existing rows and add new ones
|
|
let existingIds = Array.from(tableBody.querySelectorAll('tr')).map(row => row.id.replace('signature-row-', ''));
|
|
|
|
signatures.forEach(sig => {
|
|
const rowId = 'signature-row-' + sig.id;
|
|
let row = document.getElementById(rowId);
|
|
|
|
if (row) {
|
|
// Update existing row
|
|
updateSignatureRow(row, sig);
|
|
// Remove from existingIds
|
|
existingIds = existingIds.filter(id => id !== sig.id);
|
|
} else {
|
|
// Create new row
|
|
row = document.createElement('tr');
|
|
row.id = rowId;
|
|
updateSignatureRow(row, sig);
|
|
tableBody.appendChild(row);
|
|
}
|
|
});
|
|
|
|
// Remove rows that no longer exist
|
|
existingIds.forEach(id => {
|
|
const row = document.getElementById('signature-row-' + id);
|
|
if (row) row.remove();
|
|
});
|
|
}
|
|
|
|
function updateSignatureRow(row, sig) {
|
|
// Set row class based on status
|
|
row.className = '';
|
|
if (sig.status === 'Success') {
|
|
row.className = 'table-success';
|
|
} else if (sig.status === 'Error' || sig.status === 'Timeout') {
|
|
row.className = 'table-danger';
|
|
} else if (sig.status === 'Processing') {
|
|
row.className = 'table-warning';
|
|
} else {
|
|
row.className = 'table-light';
|
|
}
|
|
|
|
// Update row content
|
|
row.innerHTML = `
|
|
<td>${sig.id.substring(0, 8)}</td>
|
|
<td>${sig.message.length > 20 ? sig.message.substring(0, 20) + '...' : sig.message}</td>
|
|
<td>
|
|
<span class="badge rounded-pill ${getBadgeClass(sig.status)}">
|
|
${sig.status}
|
|
</span>
|
|
</td>
|
|
<td>${sig.created_at}</td>
|
|
<td>
|
|
<button class="btn btn-sm btn-info" onclick="viewSignature('${sig.id}')">
|
|
View
|
|
</button>
|
|
<button class="btn btn-sm btn-danger" onclick="deleteSignature('${sig.id}')">
|
|
Delete
|
|
</button>
|
|
</td>
|
|
`;
|
|
}
|
|
|
|
function getBadgeClass(status) {
|
|
switch(status) {
|
|
case 'Success': return 'bg-success';
|
|
case 'Error': case 'Timeout': return 'bg-danger';
|
|
case 'Processing': return 'bg-warning';
|
|
default: return 'bg-secondary';
|
|
}
|
|
}
|
|
|
|
function viewSignature(id) {
|
|
fetch(`/api/signatures/${id}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
displaySignatureDetails(data.request);
|
|
signatureDetailsModal.show();
|
|
} else {
|
|
showToast('Error: ' + data.message, 'danger');
|
|
}
|
|
})
|
|
.catch(err => {
|
|
showToast('Error loading signature details: ' + err, 'danger');
|
|
});
|
|
}
|
|
|
|
function displaySignatureDetails(signature) {
|
|
const content = document.getElementById('signature-details-content');
|
|
|
|
let statusClass = '';
|
|
if (signature.status === 'Success') statusClass = 'text-success';
|
|
else if (signature.status === 'Error' || signature.status === 'Timeout') statusClass = 'text-danger';
|
|
else if (signature.status === 'Processing') statusClass = 'text-warning';
|
|
|
|
content.innerHTML = `
|
|
<div class="card mb-3">
|
|
<div class="card-header d-flex justify-content-between">
|
|
<h5>Request ID: ${signature.id}</h5>
|
|
<h5 class="${statusClass}">Status: ${signature.status}</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="mb-3">
|
|
<h6>Public Key:</h6>
|
|
<pre class="bg-light p-2">${signature.public_key || 'N/A'}</pre>
|
|
</div>
|
|
<div class="mb-3">
|
|
<h6>Message:</h6>
|
|
<pre class="bg-light p-2">${signature.message}</pre>
|
|
</div>
|
|
${signature.signature ? `
|
|
<div class="mb-3">
|
|
<h6>Signature (base64):</h6>
|
|
<pre class="bg-light p-2">${signature.signature}</pre>
|
|
</div>` : ''}
|
|
${signature.error ? `
|
|
<div class="mb-3">
|
|
<h6 class="text-danger">Error:</h6>
|
|
<pre class="bg-light p-2">${signature.error}</pre>
|
|
</div>` : ''}
|
|
<div class="row">
|
|
<div class="col">
|
|
<p><strong>Created:</strong> ${signature.created_at}</p>
|
|
</div>
|
|
<div class="col">
|
|
<p><strong>Last Updated:</strong> ${signature.updated_at}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function deleteSignature(id) {
|
|
if (confirm('Are you sure you want to delete this signature request?')) {
|
|
fetch(`/api/signatures/${id}`, {
|
|
method: 'DELETE'
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
showToast(data.message, 'info');
|
|
refreshSignatures(); // Refresh immediately
|
|
} else {
|
|
showToast('Error: ' + data.message, 'danger');
|
|
}
|
|
})
|
|
.catch(err => {
|
|
showToast('Error deleting signature: ' + err, 'danger');
|
|
});
|
|
}
|
|
}
|
|
|
|
// Override console.log to show toast messages
|
|
const originalConsoleLog = console.log;
|
|
const originalConsoleError = console.error;
|
|
|
|
console.log = function(message) {
|
|
// Call the original console.log
|
|
originalConsoleLog.apply(console, arguments);
|
|
// Show toast with the message
|
|
showToast(message, 'info');
|
|
};
|
|
|
|
console.error = function(message) {
|
|
// Call the original console.error
|
|
originalConsoleError.apply(console, arguments);
|
|
// Show toast with the error message
|
|
showToast(message, 'danger');
|
|
};
|
|
|
|
function showToast(message, type = 'info') {
|
|
// Create toast element
|
|
const toastId = 'toast-' + Date.now();
|
|
const toastElement = document.createElement('div');
|
|
toastElement.id = toastId;
|
|
toastElement.className = 'toast w-100';
|
|
toastElement.setAttribute('role', 'alert');
|
|
toastElement.setAttribute('aria-live', 'assertive');
|
|
toastElement.setAttribute('aria-atomic', 'true');
|
|
|
|
// Set toast content
|
|
toastElement.innerHTML = `
|
|
<div class="toast-header bg-${type} text-white">
|
|
<strong class="me-auto">${type === 'danger' ? 'Error' : 'Info'}</strong>
|
|
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
|
</div>
|
|
<div class="toast-body">
|
|
${message}
|
|
</div>
|
|
`;
|
|
|
|
// Append to container
|
|
document.querySelector('.toast-container').appendChild(toastElement);
|
|
|
|
// Initialize and show the toast
|
|
const toast = new bootstrap.Toast(toastElement, {
|
|
autohide: true,
|
|
delay: 5000
|
|
});
|
|
toast.show();
|
|
|
|
// Remove toast after it's hidden
|
|
toastElement.addEventListener('hidden.bs.toast', () => {
|
|
toastElement.remove();
|
|
});
|
|
}
|
|
|
|
// Test toast
|
|
console.log('Web app loaded successfully!');
|
|
</script>
|
|
</body>
|
|
</html>
|