hostbasket/sigsocket/examples/web_app/templates/index.html
2025-05-19 14:48:40 +03:00

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>