implement signature requests over ws
This commit is contained in:
2491
sigsocket/examples/web_app/Cargo.lock
generated
Normal file
2491
sigsocket/examples/web_app/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
sigsocket/examples/web_app/Cargo.toml
Normal file
21
sigsocket/examples/web_app/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "sigsocket-web-example"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
sigsocket = { path = "../.." }
|
||||
actix-web = "4.3.1"
|
||||
actix-rt = "2.8.0"
|
||||
actix-files = "0.6.2"
|
||||
actix-web-actors = "4.2.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
env_logger = "0.10.0"
|
||||
log = "0.4"
|
||||
tera = "1.19.0"
|
||||
tokio = { version = "1.28.0", features = ["full"] }
|
||||
dotenv = "0.15.0"
|
||||
hex = "0.4.3"
|
||||
base64 = "0.13.0"
|
||||
uuid = { version = "1.0", features = ["v4"] }
|
439
sigsocket/examples/web_app/src/main.rs
Normal file
439
sigsocket/examples/web_app/src/main.rs
Normal file
@@ -0,0 +1,439 @@
|
||||
use actix_files as fs;
|
||||
use actix_web::{web, App, HttpServer, Responder, HttpResponse, Result};
|
||||
use actix_web_actors::ws;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tera::{Tera, Context};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use sigsocket::service::SigSocketService;
|
||||
use sigsocket::registry::ConnectionRegistry;
|
||||
use std::sync::RwLock;
|
||||
use log::{info, error};
|
||||
use hex;
|
||||
use base64;
|
||||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::task;
|
||||
use serde_json::json;
|
||||
|
||||
// Status enum to represent the current state of a signature request
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
pub enum SignatureStatus {
|
||||
Pending, // Request is created but not yet sent to the client
|
||||
Processing, // Request is sent to the client for signing
|
||||
Success, // Signature received and verified successfully
|
||||
Error, // An error occurred during signing
|
||||
Timeout, // Request timed out waiting for signature
|
||||
}
|
||||
|
||||
// Shared state for the application
|
||||
struct AppState {
|
||||
templates: Tera,
|
||||
sigsocket_service: Arc<SigSocketService>,
|
||||
// Store all pending signature requests with their status
|
||||
signature_requests: Arc<Mutex<HashMap<String, PendingSignature>>>,
|
||||
}
|
||||
|
||||
// Structure for incoming sign requests
|
||||
#[derive(Deserialize)]
|
||||
struct SignRequest {
|
||||
public_key: String,
|
||||
message: String,
|
||||
}
|
||||
|
||||
// Result structure for API responses
|
||||
#[derive(Serialize, Clone)]
|
||||
struct SignResult {
|
||||
id: String, // Unique ID for this signature request
|
||||
public_key: String, // Public key of the signer
|
||||
message: String, // Original message that was signed
|
||||
status: SignatureStatus, // Current status of the request
|
||||
signature: Option<String>, // Signature if available
|
||||
error: Option<String>, // Error message if any
|
||||
created_at: String, // When the request was created (human readable)
|
||||
updated_at: String, // When the request was last updated (human readable)
|
||||
}
|
||||
|
||||
// Structure to track pending signatures
|
||||
#[derive(Clone)]
|
||||
struct PendingSignature {
|
||||
id: String, // Unique ID for this request
|
||||
public_key: String, // Public key that should sign
|
||||
message: String, // Message to be signed
|
||||
message_bytes: Vec<u8>, // Raw message bytes
|
||||
status: SignatureStatus, // Current status
|
||||
error: Option<String>, // Error message if any
|
||||
signature: Option<String>, // Signature if available
|
||||
created_at: Instant, // When the request was created
|
||||
updated_at: Instant, // When the request was last updated
|
||||
timeout_duration: Duration // How long to wait before timing out
|
||||
}
|
||||
|
||||
impl PendingSignature {
|
||||
fn new(id: String, public_key: String, message: String, message_bytes: Vec<u8>) -> Self {
|
||||
let now = Instant::now();
|
||||
PendingSignature {
|
||||
id,
|
||||
public_key,
|
||||
message,
|
||||
message_bytes,
|
||||
status: SignatureStatus::Pending,
|
||||
signature: None,
|
||||
error: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
timeout_duration: Duration::from_secs(60), // Default 60-second timeout
|
||||
}
|
||||
}
|
||||
|
||||
fn to_result(&self) -> SignResult {
|
||||
SignResult {
|
||||
id: self.id.clone(),
|
||||
public_key: self.public_key.clone(),
|
||||
message: self.message.clone(),
|
||||
status: self.status.clone(),
|
||||
signature: self.signature.clone(),
|
||||
error: self.error.clone(),
|
||||
created_at: format!("{}s ago", self.created_at.elapsed().as_secs()),
|
||||
updated_at: format!("{}s ago", self.updated_at.elapsed().as_secs()),
|
||||
}
|
||||
}
|
||||
|
||||
fn update_status(&mut self, status: SignatureStatus) {
|
||||
self.status = status;
|
||||
self.updated_at = Instant::now();
|
||||
}
|
||||
|
||||
fn set_success(&mut self, signature: String) {
|
||||
self.signature = Some(signature);
|
||||
self.update_status(SignatureStatus::Success);
|
||||
}
|
||||
|
||||
fn set_error(&mut self, error: String) {
|
||||
self.error = Some(error);
|
||||
self.update_status(SignatureStatus::Error);
|
||||
}
|
||||
|
||||
fn is_timed_out(&self) -> bool {
|
||||
self.created_at.elapsed() > self.timeout_duration
|
||||
}
|
||||
}
|
||||
|
||||
// Controller for the index page
|
||||
async fn index(data: web::Data<AppState>) -> Result<HttpResponse> {
|
||||
let mut context = Context::new();
|
||||
|
||||
// Add all signature requests to the context
|
||||
let signature_requests = data.signature_requests.lock().unwrap();
|
||||
|
||||
// Convert the pending signatures to results for the template
|
||||
let mut pending_sigs: Vec<&PendingSignature> = signature_requests.values().collect();
|
||||
|
||||
// Sort by created_at date (newest first)
|
||||
pending_sigs.sort_by(|a, b| b.created_at.cmp(&a.created_at));
|
||||
|
||||
// Convert to results after sorting
|
||||
let results: Vec<SignResult> = pending_sigs.iter()
|
||||
.map(|sig| sig.to_result())
|
||||
.collect();
|
||||
|
||||
context.insert("signature_requests", &results);
|
||||
context.insert("has_requests", &!results.is_empty());
|
||||
|
||||
let rendered = data.templates.render("index.html", &context)
|
||||
.map_err(|e| {
|
||||
eprintln!("Template error: {}", e);
|
||||
actix_web::error::ErrorInternalServerError("Template error")
|
||||
})?;
|
||||
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
|
||||
}
|
||||
|
||||
// Controller for the sign endpoint
|
||||
async fn sign(
|
||||
data: web::Data<AppState>,
|
||||
form: web::Form<SignRequest>,
|
||||
) -> impl Responder {
|
||||
let message = form.message.clone();
|
||||
let public_key = form.public_key.clone();
|
||||
|
||||
info!("Received sign request for public key: {}", &public_key);
|
||||
info!("Message to sign: {}", &message);
|
||||
|
||||
// Generate a unique ID for this signature request
|
||||
let request_id = Uuid::new_v4().to_string();
|
||||
|
||||
// Log the message bytes
|
||||
let message_bytes = message.as_bytes().to_vec();
|
||||
info!("Message bytes: {:?}", message_bytes);
|
||||
info!("Message hex: {}", hex::encode(&message_bytes));
|
||||
|
||||
// Create a new pending signature request
|
||||
let pending = PendingSignature::new(
|
||||
request_id.clone(),
|
||||
public_key.clone(),
|
||||
message.clone(),
|
||||
message_bytes.clone()
|
||||
);
|
||||
|
||||
// Add the pending request to our state
|
||||
{
|
||||
let mut signature_requests = data.signature_requests.lock().unwrap();
|
||||
signature_requests.insert(request_id.clone(), pending);
|
||||
info!("Added new pending signature request: {}", request_id);
|
||||
}
|
||||
|
||||
// Clone what we need for the async task
|
||||
let request_id_clone = request_id.clone();
|
||||
let service = data.sigsocket_service.clone();
|
||||
let signature_requests = data.signature_requests.clone();
|
||||
|
||||
// Spawn an async task to handle the signature request
|
||||
task::spawn(async move {
|
||||
info!("Starting async signature task for request: {}", request_id_clone);
|
||||
|
||||
// Update status to Processing
|
||||
{
|
||||
let mut requests = signature_requests.lock().unwrap();
|
||||
if let Some(request) = requests.get_mut(&request_id_clone) {
|
||||
request.update_status(SignatureStatus::Processing);
|
||||
}
|
||||
}
|
||||
|
||||
// Send the message to be signed via SigSocket
|
||||
info!("Sending message to SigSocket service for signing...");
|
||||
match service.send_to_sign(&public_key, &message_bytes).await {
|
||||
Ok((response_bytes, signature)) => {
|
||||
// Successfully received a signature
|
||||
let signature_base64 = base64::encode(&signature);
|
||||
let message_base64 = base64::encode(&message_bytes);
|
||||
|
||||
// Format in the expected dot-separated format: base64_message.base64_signature
|
||||
let full_signature = format!("{}.{}", message_base64, signature_base64);
|
||||
|
||||
info!("Successfully received signature response for request: {}", request_id_clone);
|
||||
info!("Message base64: {}", message_base64);
|
||||
info!("Signature base64: {}", signature_base64);
|
||||
info!("Full signature (dot format): {}", full_signature);
|
||||
|
||||
// Update the signature request with the successful result
|
||||
let mut requests = signature_requests.lock().unwrap();
|
||||
if let Some(request) = requests.get_mut(&request_id_clone) {
|
||||
request.set_success(signature_base64);
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
// Error occurred
|
||||
error!("Error during signature process for request {}: {:?}", request_id_clone, err);
|
||||
|
||||
// Update the signature request with the error
|
||||
let mut requests = signature_requests.lock().unwrap();
|
||||
if let Some(request) = requests.get_mut(&request_id_clone) {
|
||||
request.set_error(format!("Error: {:?}", err));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Return JSON response if it's an AJAX request, otherwise redirect
|
||||
if is_ajax_request(&form) {
|
||||
// Return JSON response for AJAX requests
|
||||
HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.json(json!({
|
||||
"status": "pending",
|
||||
"requestId": request_id,
|
||||
"message": "Signature request added to queue"
|
||||
}))
|
||||
} else {
|
||||
// Redirect back to the index page
|
||||
HttpResponse::SeeOther()
|
||||
.append_header(("Location", "/"))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to check if this is an AJAX request
|
||||
fn is_ajax_request(_form: &web::Form<SignRequest>) -> bool {
|
||||
// For simplicity, we'll always return false for now
|
||||
// In a real application, you would check headers like X-Requested-With
|
||||
false
|
||||
}
|
||||
|
||||
// WebSocket handler for SigSocket connections
|
||||
async fn websocket_handler(
|
||||
req: actix_web::HttpRequest,
|
||||
stream: actix_web::web::Payload,
|
||||
service: web::Data<Arc<SigSocketService>>,
|
||||
) -> Result<HttpResponse> {
|
||||
// Create a new SigSocket handler
|
||||
let handler = service.create_websocket_handler();
|
||||
|
||||
// Start WebSocket connection
|
||||
ws::start(handler, &req, stream)
|
||||
}
|
||||
|
||||
// Status endpoint for SigSocket server
|
||||
async fn status_endpoint(service: web::Data<Arc<SigSocketService>>) -> impl Responder {
|
||||
// Get the connection count
|
||||
match service.connection_count() {
|
||||
Ok(count) => {
|
||||
// Return JSON response with status info
|
||||
web::Json(json!({
|
||||
"status": "online",
|
||||
"active_connections": count,
|
||||
"version": env!("CARGO_PKG_VERSION"),
|
||||
}))
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Error getting connection count: {:?}", e);
|
||||
// Return error status
|
||||
web::Json(json!({
|
||||
"status": "error",
|
||||
"error": format!("{:?}", e),
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get status of a specific signature request or all requests
|
||||
async fn signature_status(
|
||||
data: web::Data<AppState>,
|
||||
path: web::Path<(String,)>,
|
||||
) -> impl Responder {
|
||||
let request_id = &path.0;
|
||||
|
||||
// If the request_id is "all", return all requests
|
||||
if request_id == "all" {
|
||||
let signature_requests = data.signature_requests.lock().unwrap();
|
||||
|
||||
// Convert the pending signatures to results for the API
|
||||
let results: Vec<SignResult> = signature_requests.values()
|
||||
.map(|sig| sig.to_result())
|
||||
.collect();
|
||||
|
||||
return web::Json(json!({
|
||||
"status": "success",
|
||||
"count": results.len(),
|
||||
"requests": results
|
||||
}));
|
||||
}
|
||||
|
||||
// Otherwise, find the specific request
|
||||
let signature_requests = data.signature_requests.lock().unwrap();
|
||||
|
||||
if let Some(request) = signature_requests.get(request_id) {
|
||||
web::Json(json!({
|
||||
"status": "success",
|
||||
"request": request.to_result()
|
||||
}))
|
||||
} else {
|
||||
web::Json(json!({
|
||||
"status": "error",
|
||||
"message": format!("No signature request found with ID: {}", request_id)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// Delete a signature request
|
||||
async fn delete_signature(
|
||||
data: web::Data<AppState>,
|
||||
path: web::Path<(String,)>,
|
||||
) -> impl Responder {
|
||||
let request_id = &path.0;
|
||||
|
||||
let mut signature_requests = data.signature_requests.lock().unwrap();
|
||||
|
||||
if let Some(_) = signature_requests.remove(request_id) {
|
||||
web::Json(json!({
|
||||
"status": "success",
|
||||
"message": format!("Signature request {} deleted", request_id)
|
||||
}))
|
||||
} else {
|
||||
web::Json(json!({
|
||||
"status": "error",
|
||||
"message": format!("No signature request found with ID: {}", request_id)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// Task to check for timed-out signature requests
|
||||
async fn check_timeouts(signature_requests: Arc<Mutex<HashMap<String, PendingSignature>>>) {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
|
||||
// Check for timed-out requests
|
||||
let mut requests = signature_requests.lock().unwrap();
|
||||
let timed_out: Vec<String> = requests.iter()
|
||||
.filter(|(_, req)| req.status == SignatureStatus::Pending || req.status == SignatureStatus::Processing)
|
||||
.filter(|(_, req)| req.is_timed_out())
|
||||
.map(|(id, _)| id.clone())
|
||||
.collect();
|
||||
|
||||
// Update timed-out requests
|
||||
for id in timed_out {
|
||||
if let Some(req) = requests.get_mut(&id) {
|
||||
req.error = Some("Request timed out waiting for signature".to_string());
|
||||
req.update_status(SignatureStatus::Timeout);
|
||||
info!("Signature request {} timed out", id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
// Setup logger
|
||||
env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));
|
||||
|
||||
// Initialize templates
|
||||
let mut tera = Tera::default();
|
||||
tera.add_raw_templates(vec![
|
||||
("index.html", include_str!("../templates/index.html")),
|
||||
]).unwrap();
|
||||
|
||||
// Initialize SigSocket registry and service
|
||||
let registry = Arc::new(RwLock::new(ConnectionRegistry::new()));
|
||||
let sigsocket_service = Arc::new(SigSocketService::new(registry.clone()));
|
||||
|
||||
// Initialize signature requests tracking
|
||||
let signature_requests = Arc::new(Mutex::new(HashMap::new()));
|
||||
|
||||
// Start the timeout checking task
|
||||
let timeout_checker_requests = signature_requests.clone();
|
||||
tokio::spawn(async move {
|
||||
check_timeouts(timeout_checker_requests).await;
|
||||
});
|
||||
|
||||
// Shared application state
|
||||
let app_state = web::Data::new(AppState {
|
||||
templates: tera,
|
||||
sigsocket_service: sigsocket_service.clone(),
|
||||
signature_requests: signature_requests.clone(),
|
||||
});
|
||||
|
||||
info!("Web App server starting on http://127.0.0.1:8080");
|
||||
info!("SigSocket WebSocket endpoint available at ws://127.0.0.1:8080/ws");
|
||||
|
||||
// Start the web server with both our regular routes and the SigSocket WebSocket handler
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.app_data(app_state.clone())
|
||||
.app_data(web::Data::new(sigsocket_service.clone()))
|
||||
// Regular web app routes
|
||||
.route("/", web::get().to(index))
|
||||
.route("/sign", web::post().to(sign))
|
||||
// SigSocket WebSocket handler
|
||||
.route("/ws", web::get().to(websocket_handler))
|
||||
// Status endpoints
|
||||
.route("/sigsocket/status", web::get().to(status_endpoint))
|
||||
// Signature API endpoints
|
||||
.route("/api/signatures/{id}", web::get().to(signature_status))
|
||||
.route("/api/signatures/{id}", web::delete().to(delete_signature))
|
||||
// Static files
|
||||
.service(fs::Files::new("/static", "./static"))
|
||||
})
|
||||
.bind("127.0.0.1:8080")?
|
||||
.run()
|
||||
.await
|
||||
}
|
462
sigsocket/examples/web_app/templates/index.html
Normal file
462
sigsocket/examples/web_app/templates/index.html
Normal file
@@ -0,0 +1,462 @@
|
||||
<!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>
|
Reference in New Issue
Block a user