implement signature requests over ws
This commit is contained in:
		
							
								
								
									
										71
									
								
								sigsocket/examples/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								sigsocket/examples/README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,71 @@
 | 
			
		||||
# SigSocket Examples
 | 
			
		||||
 | 
			
		||||
This directory contains example applications demonstrating how to use the SigSocket library for cryptographic signing operations using WebSockets.
 | 
			
		||||
 | 
			
		||||
## Overview
 | 
			
		||||
 | 
			
		||||
These examples demonstrate a common workflow:
 | 
			
		||||
 | 
			
		||||
1. **Web Application with Integrated SigSocket Server**: An Actix-based web server that both serves the web UI and runs the SigSocket WebSocket server for handling connections and signing requests.
 | 
			
		||||
2. **Client Application**: A web interface that connects to the SigSocket WebSocket endpoint, receives signing requests, and submits signatures.
 | 
			
		||||
 | 
			
		||||
## Directory Structure
 | 
			
		||||
 | 
			
		||||
- `web_app/`: The web application with integrated SigSocket server
 | 
			
		||||
- `client_app/`: The client application that signs messages
 | 
			
		||||
 | 
			
		||||
## Running the Examples
 | 
			
		||||
 | 
			
		||||
You only need to run two components:
 | 
			
		||||
 | 
			
		||||
### 1. Start the Web Application with Integrated SigSocket Server
 | 
			
		||||
 | 
			
		||||
Start the web application which also runs the SigSocket server:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
cd /path/to/sigsocket/examples/web_app
 | 
			
		||||
cargo run
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
This will start a web interface at http://127.0.0.1:8080 where you can submit messages to be signed. It also starts the SigSocket WebSocket server at ws://127.0.0.1:8080/ws.
 | 
			
		||||
 | 
			
		||||
### 2. Start the Client Application
 | 
			
		||||
 | 
			
		||||
The client application connects to the WebSocket endpoint and waits for signing requests:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
cd /path/to/sigsocket/examples/client_app
 | 
			
		||||
cargo run
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
This will start a web interface at http://127.0.0.1:8082 where you can see signing requests and approve them.
 | 
			
		||||
 | 
			
		||||
## Using the Applications
 | 
			
		||||
 | 
			
		||||
1. Open the client app in a browser at http://127.0.0.1:8082
 | 
			
		||||
2. Note the public key displayed on the page
 | 
			
		||||
3. Open the web app in another browser window at http://127.0.0.1:8080
 | 
			
		||||
4. Enter the public key from step 2 into the "Public Key" field
 | 
			
		||||
5. Enter a message to be signed and submit the form
 | 
			
		||||
6. The message will be sent to the SigSocket server, which forwards it to the connected client
 | 
			
		||||
7. In the client app, you'll see the sign request appear - click "Sign Message" to approve
 | 
			
		||||
8. The signature will be sent back through the SigSocket server to the web app
 | 
			
		||||
9. The web app will display the signature
 | 
			
		||||
 | 
			
		||||
## How It Works
 | 
			
		||||
 | 
			
		||||
1. **SigSocket Server**: Provides a WebSocket endpoint for clients to connect and register with their public keys. It also accepts HTTP requests to sign messages with a specific client's key.
 | 
			
		||||
 | 
			
		||||
2. **Web Application**:
 | 
			
		||||
   - Provides a form for users to enter a public key and message
 | 
			
		||||
   - Uses the SigSocket service to send the message to be signed
 | 
			
		||||
   - Displays the resulting signature
 | 
			
		||||
 | 
			
		||||
3. **Client Application**:
 | 
			
		||||
   - Connects to the SigSocket server via WebSocket
 | 
			
		||||
   - Registers with a public key
 | 
			
		||||
   - Waits for signing requests
 | 
			
		||||
   - Displays incoming requests and allows the user to approve them
 | 
			
		||||
   - Signs messages using ECDSA with Secp256k1 and sends the signatures back
 | 
			
		||||
 | 
			
		||||
This demonstrates a real-world use case where a web application needs to verify a user's identity or get approval for transactions through cryptographic signatures, without having direct access to the private keys.
 | 
			
		||||
							
								
								
									
										2575
									
								
								sigsocket/examples/client_app/Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										2575
									
								
								sigsocket/examples/client_app/Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										22
									
								
								sigsocket/examples/client_app/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								sigsocket/examples/client_app/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
[package]
 | 
			
		||||
name = "sigsocket-client-example"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
edition = "2021"
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
tokio = { version = "1.28.0", features = ["full"] }
 | 
			
		||||
tokio-tungstenite = { version = "0.18.0", features = ["native-tls"] }
 | 
			
		||||
futures-util = "0.3.28"
 | 
			
		||||
serde = { version = "1.0", features = ["derive"] }
 | 
			
		||||
serde_json = "1.0"
 | 
			
		||||
log = "0.4"
 | 
			
		||||
env_logger = "0.10.0"
 | 
			
		||||
secp256k1 = { version = "0.26.0", features = ["rand-std"] }
 | 
			
		||||
sha2 = "0.10.6"
 | 
			
		||||
rand = "0.8.5"
 | 
			
		||||
hex = "0.4.3"
 | 
			
		||||
base64 = "0.21.2"
 | 
			
		||||
actix-web = "4.3.1"
 | 
			
		||||
actix-files = "0.6.2"
 | 
			
		||||
tera = "1.19.0"
 | 
			
		||||
url = "2.4.0"
 | 
			
		||||
							
								
								
									
										474
									
								
								sigsocket/examples/client_app/src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										474
									
								
								sigsocket/examples/client_app/src/main.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,474 @@
 | 
			
		||||
use actix_files as fs;
 | 
			
		||||
use actix_web::{web, App, HttpServer, Responder, HttpResponse, Result};
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use tera::{Tera, Context};
 | 
			
		||||
use std::sync::{Arc, Mutex};
 | 
			
		||||
use tokio::sync::mpsc;
 | 
			
		||||
use tokio_tungstenite::{connect_async, tungstenite};
 | 
			
		||||
use futures_util::{StreamExt, SinkExt};
 | 
			
		||||
use secp256k1::{Secp256k1, SecretKey, Message};
 | 
			
		||||
use sha2::{Sha256, Digest};
 | 
			
		||||
use url::Url;
 | 
			
		||||
use std::thread;
 | 
			
		||||
 | 
			
		||||
// Struct for representing a sign request
 | 
			
		||||
#[derive(Serialize, Deserialize, Clone, Debug)]
 | 
			
		||||
struct SignRequest {
 | 
			
		||||
    id: String,
 | 
			
		||||
    message: String,
 | 
			
		||||
    #[serde(skip)]
 | 
			
		||||
    message_raw: String, // Original base64 message for sending back in the response
 | 
			
		||||
    #[serde(skip)]
 | 
			
		||||
    message_decoded: String, // Decoded message for display
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Struct for representing the application state
 | 
			
		||||
struct AppState {
 | 
			
		||||
    templates: Tera,
 | 
			
		||||
    keypair: Arc<KeyPair>,
 | 
			
		||||
    pending_request: Arc<Mutex<Option<SignRequest>>>,
 | 
			
		||||
    websocket_sender: mpsc::Sender<WebSocketCommand>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Commands that can be sent to the WebSocket connection
 | 
			
		||||
enum WebSocketCommand {
 | 
			
		||||
    Sign { id: String, message: String, signature: Vec<u8> },
 | 
			
		||||
    Close,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Keypair for signing messages
 | 
			
		||||
struct KeyPair {
 | 
			
		||||
    secret_key: SecretKey,
 | 
			
		||||
    public_key_hex: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl KeyPair {
 | 
			
		||||
    fn new() -> Self {
 | 
			
		||||
        let secp = Secp256k1::new();
 | 
			
		||||
        let mut rng = rand::thread_rng();
 | 
			
		||||
        
 | 
			
		||||
        // Generate a new random keypair
 | 
			
		||||
        let (secret_key, public_key) = secp.generate_keypair(&mut rng);
 | 
			
		||||
        
 | 
			
		||||
        // Convert public key to hex for identification
 | 
			
		||||
        let public_key_hex = hex::encode(public_key.serialize());
 | 
			
		||||
        
 | 
			
		||||
        KeyPair {
 | 
			
		||||
            secret_key,
 | 
			
		||||
            public_key_hex,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    fn sign(&self, message: &[u8]) -> Vec<u8> {
 | 
			
		||||
        // Hash the message first (secp256k1 requires a 32-byte hash)
 | 
			
		||||
        let mut hasher = Sha256::new();
 | 
			
		||||
        hasher.update(message);
 | 
			
		||||
        let message_hash = hasher.finalize();
 | 
			
		||||
        
 | 
			
		||||
        // Create a secp256k1 message from the hash
 | 
			
		||||
        let secp_message = Message::from_slice(&message_hash).unwrap();
 | 
			
		||||
        
 | 
			
		||||
        // Sign the message
 | 
			
		||||
        let secp = Secp256k1::new();
 | 
			
		||||
        let signature = secp.sign_ecdsa(&secp_message, &self.secret_key);
 | 
			
		||||
        
 | 
			
		||||
        // Return the serialized signature
 | 
			
		||||
        signature.serialize_compact().to_vec()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Controller for the index page
 | 
			
		||||
async fn index(data: web::Data<AppState>) -> Result<HttpResponse> {
 | 
			
		||||
    let mut context = Context::new();
 | 
			
		||||
    
 | 
			
		||||
    // Add the keypair to the context
 | 
			
		||||
    context.insert("public_key", &data.keypair.public_key_hex);
 | 
			
		||||
    
 | 
			
		||||
    // Add the pending request if there is one
 | 
			
		||||
    if let Some(request) = &*data.pending_request.lock().unwrap() {
 | 
			
		||||
        context.insert("request", request);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    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_request(
 | 
			
		||||
    data: web::Data<AppState>,
 | 
			
		||||
    form: web::Form<SignRequestForm>,
 | 
			
		||||
) -> impl Responder {
 | 
			
		||||
    println!("SIGN ENDPOINT: Starting sign_request handler for form ID: {}", form.id);
 | 
			
		||||
    
 | 
			
		||||
    // Try to get a lock on the pending request
 | 
			
		||||
    println!("SIGN ENDPOINT: Attempting to acquire lock on pending_request");
 | 
			
		||||
    match data.pending_request.try_lock() {
 | 
			
		||||
        Ok(mut guard) => {
 | 
			
		||||
            // Check if we have a pending request
 | 
			
		||||
            if let Some(request) = &*guard {
 | 
			
		||||
                println!("SIGN ENDPOINT: Found pending request with ID: {}", request.id);
 | 
			
		||||
                
 | 
			
		||||
                // Get the request ID
 | 
			
		||||
                let id = request.id.clone();
 | 
			
		||||
                
 | 
			
		||||
                // Verify that the request ID matches
 | 
			
		||||
                if id == form.id {
 | 
			
		||||
                    println!("SIGN ENDPOINT: Request ID matches form ID: {}", id);
 | 
			
		||||
                    
 | 
			
		||||
                    // Sign the message
 | 
			
		||||
                    let message = request.message.as_bytes();
 | 
			
		||||
                    println!("SIGN ENDPOINT: About to sign message: {} (length: {})", 
 | 
			
		||||
                          String::from_utf8_lossy(message), message.len());
 | 
			
		||||
                    let signature = data.keypair.sign(message);
 | 
			
		||||
                    println!("SIGN ENDPOINT: Message signed successfully. Signature length: {}", signature.len());
 | 
			
		||||
                    
 | 
			
		||||
                    // Send the signature via WebSocket
 | 
			
		||||
                    println!("SIGN ENDPOINT: About to send signature via websocket channel");
 | 
			
		||||
                    match data.websocket_sender.send(WebSocketCommand::Sign { 
 | 
			
		||||
                        id: id.clone(), 
 | 
			
		||||
                        message: request.message_raw.clone(), // Include the original base64 message
 | 
			
		||||
                        signature 
 | 
			
		||||
                    }).await {
 | 
			
		||||
                        Ok(_) => {
 | 
			
		||||
                            println!("SIGN ENDPOINT: Successfully sent signature to websocket channel");
 | 
			
		||||
                        },
 | 
			
		||||
                        Err(e) => {
 | 
			
		||||
                            let error_msg = format!("Failed to send signature: {}", e);
 | 
			
		||||
                            println!("SIGN ENDPOINT ERROR: {}", error_msg);
 | 
			
		||||
                            return HttpResponse::InternalServerError()
 | 
			
		||||
                                .content_type("text/html")
 | 
			
		||||
                                .body(format!("<h1>Error sending signature</h1><p>{}</p><p><a href='/'>Return to home</a></p>", error_msg));
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    
 | 
			
		||||
                    // Clear the pending request
 | 
			
		||||
                    println!("SIGN ENDPOINT: Clearing pending request");
 | 
			
		||||
                    *guard = None;
 | 
			
		||||
                    
 | 
			
		||||
                    // Return a success page that continues to the next step
 | 
			
		||||
                    println!("SIGN ENDPOINT: Returning success response");
 | 
			
		||||
                    return HttpResponse::Ok()
 | 
			
		||||
                        .content_type("text/html")
 | 
			
		||||
                        .body(r#"<html>
 | 
			
		||||
                            <head>
 | 
			
		||||
                                <title>Signature Sent</title>
 | 
			
		||||
                                <meta http-equiv="refresh" content="2; url=/" />
 | 
			
		||||
                                <script type="text/javascript">
 | 
			
		||||
                                    console.log("Signature sent successfully, redirecting in 2 seconds...");
 | 
			
		||||
                                    setTimeout(function() { window.location.href = '/'; }, 2000);
 | 
			
		||||
                                </script>
 | 
			
		||||
                                <style>
 | 
			
		||||
                                    body { font-family: Arial, sans-serif; text-align: center; margin-top: 50px; }
 | 
			
		||||
                                    .success { color: green; }
 | 
			
		||||
                                </style>
 | 
			
		||||
                            </head>
 | 
			
		||||
                            <body>
 | 
			
		||||
                                <h1 class="success">✓ Signature Sent Successfully!</h1>
 | 
			
		||||
                                <p>Redirecting back to home page...</p>
 | 
			
		||||
                                <p><a href="/">Click here if you're not redirected automatically</a></p>
 | 
			
		||||
                            </body>
 | 
			
		||||
                        </html>"#);
 | 
			
		||||
                } else {
 | 
			
		||||
                    println!("SIGN ENDPOINT: Request ID {} does not match form ID {}", request.id, form.id);
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                println!("SIGN ENDPOINT: No pending request found");
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            let error_msg = format!("Failed to acquire lock on pending_request: {}", e);
 | 
			
		||||
            println!("SIGN ENDPOINT ERROR: {}", error_msg);
 | 
			
		||||
            return HttpResponse::InternalServerError()
 | 
			
		||||
                .content_type("text/html")
 | 
			
		||||
                .body(format!("<h1>Error processing request</h1><p>{}</p><p><a href='/'>Return to home</a></p>", error_msg));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Redirect back to the index page (if no request was found or ID didn't match)
 | 
			
		||||
    println!("SIGN ENDPOINT: No matching request found, redirecting to home");
 | 
			
		||||
    HttpResponse::SeeOther()
 | 
			
		||||
        .append_header(("Location", "/"))
 | 
			
		||||
        .finish()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Form for submitting a signature
 | 
			
		||||
#[derive(Deserialize)]
 | 
			
		||||
struct SignRequestForm {
 | 
			
		||||
    id: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WebSocket client task that connects to the SigSocket server
 | 
			
		||||
async fn websocket_client_task(
 | 
			
		||||
    keypair: Arc<KeyPair>, 
 | 
			
		||||
    pending_request: Arc<Mutex<Option<SignRequest>>>,
 | 
			
		||||
    mut command_receiver: mpsc::Receiver<WebSocketCommand>,
 | 
			
		||||
) {
 | 
			
		||||
    // Connect directly to the web app's integrated SigSocket endpoint
 | 
			
		||||
    let sigsocket_url = "ws://127.0.0.1:8080/ws";
 | 
			
		||||
    
 | 
			
		||||
    // Reconnection settings
 | 
			
		||||
    let mut retry_count = 0;
 | 
			
		||||
    const MAX_RETRY_COUNT: u32 = 10; // Reset retry counter after this many attempts
 | 
			
		||||
    const BASE_RETRY_DELAY_MS: u64 = 1000; // Start with 1 second
 | 
			
		||||
    const MAX_RETRY_DELAY_MS: u64 = 30000; // Cap at 30 seconds
 | 
			
		||||
    
 | 
			
		||||
    loop {
 | 
			
		||||
        // Calculate backoff delay with jitter for retry
 | 
			
		||||
        let delay_ms = if retry_count > 0 {
 | 
			
		||||
            let base_delay = BASE_RETRY_DELAY_MS * 2u64.pow(retry_count.min(6));
 | 
			
		||||
            let jitter = rand::random::<u64>() % 500; // Add up to 500ms of jitter
 | 
			
		||||
            (base_delay + jitter).min(MAX_RETRY_DELAY_MS)
 | 
			
		||||
        } else {
 | 
			
		||||
            0 // No delay on first attempt
 | 
			
		||||
        };
 | 
			
		||||
        
 | 
			
		||||
        if retry_count > 0 {
 | 
			
		||||
            println!("Reconnection attempt {} in {} ms...", retry_count, delay_ms);
 | 
			
		||||
            tokio::time::sleep(tokio::time::Duration::from_millis(delay_ms)).await;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Connect to the SigSocket server with timeout
 | 
			
		||||
        println!("Connecting to SigSocket server at {}", sigsocket_url);
 | 
			
		||||
        let connect_result = tokio::time::timeout(
 | 
			
		||||
            tokio::time::Duration::from_secs(10), // Connection timeout
 | 
			
		||||
            connect_async(Url::parse(sigsocket_url).unwrap())
 | 
			
		||||
        ).await;
 | 
			
		||||
        
 | 
			
		||||
        match connect_result {
 | 
			
		||||
            // Timeout error
 | 
			
		||||
            Err(_) => {
 | 
			
		||||
                eprintln!("Connection attempt timed out");
 | 
			
		||||
                retry_count = (retry_count + 1) % MAX_RETRY_COUNT;
 | 
			
		||||
                continue;
 | 
			
		||||
            },
 | 
			
		||||
            // Connection result
 | 
			
		||||
            Ok(conn_result) => match conn_result {
 | 
			
		||||
                // Connection successful
 | 
			
		||||
                Ok((mut ws_stream, _)) => {
 | 
			
		||||
                    println!("Connected to SigSocket server");
 | 
			
		||||
                    // Reset retry counter on successful connection
 | 
			
		||||
                    retry_count = 0;
 | 
			
		||||
                    
 | 
			
		||||
                    // Heartbeat functionality has been removed
 | 
			
		||||
                    println!("DEBUG: Running without heartbeat functionality");
 | 
			
		||||
                    
 | 
			
		||||
                    // Send the initial message with just the raw public key
 | 
			
		||||
                    let intro_message = keypair.public_key_hex.clone();
 | 
			
		||||
                    if let Err(e) = ws_stream.send(tungstenite::Message::Text(intro_message)).await {
 | 
			
		||||
                        eprintln!("Failed to send introduction message: {}", e);
 | 
			
		||||
                        continue;
 | 
			
		||||
                    }
 | 
			
		||||
                    
 | 
			
		||||
                    println!("Sent introduction with public key: {}", keypair.public_key_hex);
 | 
			
		||||
                    
 | 
			
		||||
                    // Last time we received a message or pong from the server
 | 
			
		||||
                    let mut last_server_response = std::time::Instant::now();
 | 
			
		||||
                    
 | 
			
		||||
                    // Process incoming messages and commands
 | 
			
		||||
                    loop {
 | 
			
		||||
                        tokio::select! {
 | 
			
		||||
                            // Handle WebSocket message
 | 
			
		||||
                            msg = ws_stream.next() => {
 | 
			
		||||
                                match msg {
 | 
			
		||||
                                    Some(Ok(tungstenite::Message::Text(text))) => {
 | 
			
		||||
                                        println!("Received message: {}", text);
 | 
			
		||||
                                        last_server_response = std::time::Instant::now();
 | 
			
		||||
                                        
 | 
			
		||||
                                        // Parse the message as a sign request
 | 
			
		||||
                                        match serde_json::from_str::<SignRequest>(&text) {
 | 
			
		||||
                                            Ok(mut request) => {
 | 
			
		||||
                                                println!("DEBUG: Successfully parsed sign request with ID: {}", request.id);
 | 
			
		||||
                                                println!("DEBUG: Base64 message: {}", request.message);
 | 
			
		||||
                                                
 | 
			
		||||
                                                // Save the original base64 message for later use in response
 | 
			
		||||
                                                request.message_raw = request.message.clone();
 | 
			
		||||
                                                
 | 
			
		||||
                                                // Decode the base64 message content
 | 
			
		||||
                                                match base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &request.message) {
 | 
			
		||||
                                                    Ok(decoded) => {
 | 
			
		||||
                                                        let decoded_text = String::from_utf8_lossy(&decoded).to_string();
 | 
			
		||||
                                                        println!("DEBUG: Decoded message: {}", decoded_text);
 | 
			
		||||
                                                        
 | 
			
		||||
                                                        // Store the decoded message for display
 | 
			
		||||
                                                        request.message_decoded = decoded_text;
 | 
			
		||||
                                                        
 | 
			
		||||
                                                        // Update the message for displaying in the UI
 | 
			
		||||
                                                        request.message = request.message_decoded.clone();
 | 
			
		||||
                                                        
 | 
			
		||||
                                                        // Store the request for display in the UI
 | 
			
		||||
                                                        *pending_request.lock().unwrap() = Some(request);
 | 
			
		||||
                                                        println!("Received signing request. Please check the web UI to approve it.");
 | 
			
		||||
                                                    },
 | 
			
		||||
                                                    Err(e) => {
 | 
			
		||||
                                                        eprintln!("Error decoding base64 message: {}", e);
 | 
			
		||||
                                                    }
 | 
			
		||||
                                                }
 | 
			
		||||
                                            },
 | 
			
		||||
                                            Err(e) => {
 | 
			
		||||
                                                eprintln!("Error parsing sign request JSON: {}", e);
 | 
			
		||||
                                                eprintln!("Raw message: {}", text);
 | 
			
		||||
                                            }
 | 
			
		||||
                                        }
 | 
			
		||||
                                    },
 | 
			
		||||
                                    Some(Ok(tungstenite::Message::Ping(data))) => {
 | 
			
		||||
                                        // Respond to ping with pong
 | 
			
		||||
                                        last_server_response = std::time::Instant::now();
 | 
			
		||||
                                        if let Err(e) = ws_stream.send(tungstenite::Message::Pong(data)).await {
 | 
			
		||||
                                            eprintln!("Failed to send pong: {}", e);
 | 
			
		||||
                                            break;
 | 
			
		||||
                                        }
 | 
			
		||||
                                    },
 | 
			
		||||
                                    Some(Ok(tungstenite::Message::Pong(_))) => {
 | 
			
		||||
                                        // Got pong response from the server
 | 
			
		||||
                                        last_server_response = std::time::Instant::now();
 | 
			
		||||
                                    },
 | 
			
		||||
                                    Some(Ok(_)) => {
 | 
			
		||||
                                        // Ignore other types of messages
 | 
			
		||||
                                        last_server_response = std::time::Instant::now();
 | 
			
		||||
                                    },
 | 
			
		||||
                                    Some(Err(e)) => {
 | 
			
		||||
                                        eprintln!("WebSocket error: {}", e);
 | 
			
		||||
                                        break;
 | 
			
		||||
                                    },
 | 
			
		||||
                                    None => {
 | 
			
		||||
                                        eprintln!("WebSocket connection closed");
 | 
			
		||||
                                        break;
 | 
			
		||||
                                    },
 | 
			
		||||
                                }
 | 
			
		||||
                            },
 | 
			
		||||
                            
 | 
			
		||||
                            // Heartbeat functionality has been removed
 | 
			
		||||
                            
 | 
			
		||||
                            // Handle signing command from the web interface
 | 
			
		||||
                            cmd = command_receiver.recv() => {
 | 
			
		||||
                                match cmd {
 | 
			
		||||
                                    Some(WebSocketCommand::Sign { id, message, signature }) => {
 | 
			
		||||
                                        println!("DEBUG: Signing request ID: {}", id);
 | 
			
		||||
                                        println!("DEBUG: Raw signature bytes: {:?}", signature);
 | 
			
		||||
                                        println!("DEBUG: Using message from command: {}", message);
 | 
			
		||||
                                        
 | 
			
		||||
                                        // Convert signature bytes to base64
 | 
			
		||||
                                        let sig_base64 = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &signature);
 | 
			
		||||
                                        println!("DEBUG: Base64 signature: {}", sig_base64);
 | 
			
		||||
                                        
 | 
			
		||||
                                        // Create a JSON response with explicit ID and message/signature fields
 | 
			
		||||
                                        let response = format!("{{\"id\": \"{}\", \"message\": \"{}\", \"signature\": \"{}\"}}", 
 | 
			
		||||
                                                             id, message, sig_base64);
 | 
			
		||||
                                        println!("DEBUG: Preparing to send JSON response: {}", response);
 | 
			
		||||
                                        println!("DEBUG: Response length: {} bytes", response.len());
 | 
			
		||||
                                        
 | 
			
		||||
                                        // Log that we're about to send on the WebSocket connection
 | 
			
		||||
                                        println!("DEBUG: About to send on WebSocket connection");
 | 
			
		||||
                                        
 | 
			
		||||
                                        // Send the signature response right away - with extra logging
 | 
			
		||||
                                        println!("!!!! ATTEMPTING TO SEND SIGNATURE RESPONSE NOW !!!!");
 | 
			
		||||
                                        match ws_stream.send(tungstenite::Message::Text(response.clone())).await {
 | 
			
		||||
                                            Ok(_) => {
 | 
			
		||||
                                                last_server_response = std::time::Instant::now();
 | 
			
		||||
                                                println!("!!!! SUCCESSFULLY SENT SIGNATURE RESPONSE !!!!");
 | 
			
		||||
                                                println!("!!!! SIGNATURE SENT FOR REQUEST ID: {} !!!!", id);
 | 
			
		||||
                                                
 | 
			
		||||
                                                // Clear the pending request after successful signature
 | 
			
		||||
                                                *pending_request.lock().unwrap() = None;
 | 
			
		||||
                                                
 | 
			
		||||
                                                // Send another simple message to confirm the connection is still working
 | 
			
		||||
                                                if let Err(e) = ws_stream.send(tungstenite::Message::Text("CONFIRM_SIGNATURE_SENT".to_string())).await {
 | 
			
		||||
                                                    println!("DEBUG: Failed to send confirmation message: {}", e);
 | 
			
		||||
                                                } else {
 | 
			
		||||
                                                    println!("DEBUG: Sent confirmation message after signature");
 | 
			
		||||
                                                }
 | 
			
		||||
                                            },
 | 
			
		||||
                                            Err(e) => {
 | 
			
		||||
                                                eprintln!("!!!! FAILED TO SEND SIGNATURE RESPONSE: {} !!!!", e);
 | 
			
		||||
                                                // Try to reconnect or recover
 | 
			
		||||
                                                println!("DEBUG: Attempting to diagnose connection issue...");
 | 
			
		||||
                                                break;
 | 
			
		||||
                                            }
 | 
			
		||||
                                        }
 | 
			
		||||
                                    },
 | 
			
		||||
                                    Some(WebSocketCommand::Close) => {
 | 
			
		||||
                                        println!("DEBUG: Received close command, closing connection");
 | 
			
		||||
                                        break;
 | 
			
		||||
                                    },
 | 
			
		||||
                                    None => {
 | 
			
		||||
                                        eprintln!("Command channel closed");
 | 
			
		||||
                                        break;
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    
 | 
			
		||||
                    // Connection loop has ended, will attempt to reconnect
 | 
			
		||||
                    println!("WebSocket connection closed, will attempt to reconnect...");
 | 
			
		||||
                },
 | 
			
		||||
                
 | 
			
		||||
                // Connection error
 | 
			
		||||
                Err(e) => {
 | 
			
		||||
                    eprintln!("Failed to connect to SigSocket server: {}", e);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Increment retry counter but don't exceed MAX_RETRY_COUNT
 | 
			
		||||
        retry_count = (retry_count + 1) % MAX_RETRY_COUNT;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[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();
 | 
			
		||||
    
 | 
			
		||||
    // Generate a keypair for signing
 | 
			
		||||
    let keypair = Arc::new(KeyPair::new());
 | 
			
		||||
    println!("Generated keypair with public key: {}", keypair.public_key_hex);
 | 
			
		||||
    
 | 
			
		||||
    // Create a channel for sending commands to the WebSocket client
 | 
			
		||||
    let (command_sender, command_receiver) = mpsc::channel::<WebSocketCommand>(32);
 | 
			
		||||
    
 | 
			
		||||
    // Create the pending request mutex
 | 
			
		||||
    let pending_request = Arc::new(Mutex::new(None::<SignRequest>));
 | 
			
		||||
    
 | 
			
		||||
    // Spawn the WebSocket client task
 | 
			
		||||
    let ws_keypair = keypair.clone();
 | 
			
		||||
    let ws_pending_request = pending_request.clone();
 | 
			
		||||
    tokio::spawn(async move {
 | 
			
		||||
        websocket_client_task(ws_keypair, ws_pending_request, command_receiver).await;
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Create the app state
 | 
			
		||||
    let app_state = web::Data::new(AppState {
 | 
			
		||||
        templates: tera,
 | 
			
		||||
        keypair,
 | 
			
		||||
        pending_request,
 | 
			
		||||
        websocket_sender: command_sender,
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    println!("Client App server starting on http://127.0.0.1:8082");
 | 
			
		||||
    
 | 
			
		||||
    // Start the web server
 | 
			
		||||
    HttpServer::new(move || {
 | 
			
		||||
        App::new()
 | 
			
		||||
            .app_data(app_state.clone())
 | 
			
		||||
            // Register routes
 | 
			
		||||
            .route("/", web::get().to(index))
 | 
			
		||||
            .route("/sign", web::post().to(sign_request))
 | 
			
		||||
            // Static files
 | 
			
		||||
            .service(fs::Files::new("/static", "./static"))
 | 
			
		||||
    })
 | 
			
		||||
    .bind("127.0.0.1:8082")?
 | 
			
		||||
    .run()
 | 
			
		||||
    .await
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										204
									
								
								sigsocket/examples/client_app/templates/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										204
									
								
								sigsocket/examples/client_app/templates/index.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,204 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
<head>
 | 
			
		||||
    <meta charset="UTF-8">
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
    <title>SigSocket Client Demo</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: 900px;
 | 
			
		||||
            margin: 0 auto;
 | 
			
		||||
            padding: 20px;
 | 
			
		||||
            line-height: 1.6;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        h1, h2 {
 | 
			
		||||
            color: #333;
 | 
			
		||||
            text-align: center;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        .status-box {
 | 
			
		||||
            text-align: center;
 | 
			
		||||
            padding: 15px;
 | 
			
		||||
            margin-bottom: 30px;
 | 
			
		||||
            border-radius: 5px;
 | 
			
		||||
            background-color: #f5f5f5;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        .status-connected {
 | 
			
		||||
            background-color: #d4edda;
 | 
			
		||||
            color: #155724;
 | 
			
		||||
            border: 1px solid #c3e6cb;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        .client-info {
 | 
			
		||||
            margin-bottom: 30px;
 | 
			
		||||
            padding: 15px;
 | 
			
		||||
            border: 1px solid #ddd;
 | 
			
		||||
            border-radius: 5px;
 | 
			
		||||
            background-color: #f9f9f9;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        .keypair-info {
 | 
			
		||||
            font-family: monospace;
 | 
			
		||||
            word-break: break-all;
 | 
			
		||||
            margin: 10px 0;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        .request-panel {
 | 
			
		||||
            padding: 20px;
 | 
			
		||||
            border: 1px solid #ddd;
 | 
			
		||||
            border-radius: 5px;
 | 
			
		||||
            margin-bottom: 30px;
 | 
			
		||||
            background-color: #fff;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        .message-box {
 | 
			
		||||
            font-family: monospace;
 | 
			
		||||
            background-color: #f8f9fa;
 | 
			
		||||
            padding: 15px;
 | 
			
		||||
            border: 1px solid #ddd;
 | 
			
		||||
            border-radius: 4px;
 | 
			
		||||
            margin: 15px 0;
 | 
			
		||||
            white-space: pre-wrap;
 | 
			
		||||
            word-break: break-all;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        .no-requests {
 | 
			
		||||
            text-align: center;
 | 
			
		||||
            padding: 30px;
 | 
			
		||||
            color: #6c757d;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        button {
 | 
			
		||||
            background-color: #4CAF50;
 | 
			
		||||
            color: white;
 | 
			
		||||
            padding: 10px 15px;
 | 
			
		||||
            border: none;
 | 
			
		||||
            border-radius: 4px;
 | 
			
		||||
            cursor: pointer;
 | 
			
		||||
            font-size: 16px;
 | 
			
		||||
            display: block;
 | 
			
		||||
            margin: 0 auto;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        button:hover {
 | 
			
		||||
            background-color: #45a049;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        .footer {
 | 
			
		||||
            text-align: center;
 | 
			
		||||
            margin-top: 30px;
 | 
			
		||||
            color: #6c757d;
 | 
			
		||||
            font-size: 0.9em;
 | 
			
		||||
        }
 | 
			
		||||
    </style>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
    <h1>SigSocket Client Demo</h1>
 | 
			
		||||
    
 | 
			
		||||
    <div class="status-box status-connected">
 | 
			
		||||
        <p><strong>Status:</strong> Connected to SigSocket Server</p>
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
    <div class="client-info">
 | 
			
		||||
        <h2>Client Information</h2>
 | 
			
		||||
        <p><strong>Public Key:</strong></p>
 | 
			
		||||
        <p class="keypair-info">{{ public_key }}</p>
 | 
			
		||||
        <p>This public key is used to identify this client to the SigSocket server.</p>
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
    {% if request %}
 | 
			
		||||
        <div class="request-panel">
 | 
			
		||||
            <h2>Pending Sign Request</h2>
 | 
			
		||||
            <p><strong>Request ID:</strong> {{ request.id }}</p>
 | 
			
		||||
            
 | 
			
		||||
            <p><strong>Message to Sign:</strong></p>
 | 
			
		||||
            <div class="message-box">{{ request.message }}</div>
 | 
			
		||||
            
 | 
			
		||||
            <form action="/sign" method="post">
 | 
			
		||||
                <input type="hidden" name="id" value="{{ request.id }}">
 | 
			
		||||
                <button type="submit">Sign Message</button>
 | 
			
		||||
            </form>
 | 
			
		||||
        </div>
 | 
			
		||||
    {% else %}
 | 
			
		||||
        <div class="request-panel no-requests">
 | 
			
		||||
            <h2>No Pending Requests</h2>
 | 
			
		||||
            <p>Waiting for a sign request from the SigSocket server...</p>
 | 
			
		||||
        </div>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
    
 | 
			
		||||
    <div class="footer">
 | 
			
		||||
        <p>This client connects to a SigSocket server via WebSocket and responds to signature requests.</p>
 | 
			
		||||
        <p>The signing is done using Secp256k1 ECDSA with a randomly generated keypair.</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>
 | 
			
		||||
        // 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('Client app loaded successfully!');
 | 
			
		||||
    </script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										53
									
								
								sigsocket/examples/run_example.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										53
									
								
								sigsocket/examples/run_example.sh
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,53 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
 | 
			
		||||
# Script to run both the SigSocket web app and client app and open them in the browser
 | 
			
		||||
 | 
			
		||||
# Set the base directory
 | 
			
		||||
BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
 | 
			
		||||
WEB_APP_DIR="$BASE_DIR/web_app"
 | 
			
		||||
CLIENT_APP_DIR="$BASE_DIR/client_app"
 | 
			
		||||
 | 
			
		||||
# Colors for terminal output
 | 
			
		||||
GREEN='\033[0;32m'
 | 
			
		||||
YELLOW='\033[1;33m'
 | 
			
		||||
NC='\033[0m' # No Color
 | 
			
		||||
 | 
			
		||||
# Function to kill background processes on exit
 | 
			
		||||
cleanup() {
 | 
			
		||||
  echo -e "${YELLOW}Stopping all processes...${NC}"
 | 
			
		||||
  kill $(jobs -p) 2>/dev/null
 | 
			
		||||
  exit 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# Set up cleanup on script termination
 | 
			
		||||
trap cleanup INT TERM EXIT
 | 
			
		||||
 | 
			
		||||
echo -e "${GREEN}Starting SigSocket Demo Applications...${NC}"
 | 
			
		||||
 | 
			
		||||
# Start the web app in the background
 | 
			
		||||
echo -e "${GREEN}Starting Web App (http://127.0.0.1:8080)...${NC}"
 | 
			
		||||
cd "$WEB_APP_DIR" && cargo run &
 | 
			
		||||
 | 
			
		||||
# Wait for the web app to start (adjust time as needed)
 | 
			
		||||
echo "Waiting for web app to initialize..."
 | 
			
		||||
sleep 5
 | 
			
		||||
 | 
			
		||||
# Start the client app in the background
 | 
			
		||||
echo -e "${GREEN}Starting Client App (http://127.0.0.1:8082)...${NC}"
 | 
			
		||||
cd "$CLIENT_APP_DIR" && cargo run &
 | 
			
		||||
 | 
			
		||||
# Wait for the client app to start
 | 
			
		||||
echo "Waiting for client app to initialize..."
 | 
			
		||||
sleep 5
 | 
			
		||||
 | 
			
		||||
# Open browsers (works on macOS)
 | 
			
		||||
echo -e "${GREEN}Opening browsers...${NC}"
 | 
			
		||||
open "http://127.0.0.1:8080" # Web App
 | 
			
		||||
sleep 1
 | 
			
		||||
open "http://127.0.0.1:8082" # Client App
 | 
			
		||||
 | 
			
		||||
echo -e "${GREEN}SigSocket demo is running!${NC}"
 | 
			
		||||
echo -e "${YELLOW}Press Ctrl+C to stop all applications${NC}"
 | 
			
		||||
 | 
			
		||||
# Keep the script running until Ctrl+C
 | 
			
		||||
wait
 | 
			
		||||
							
								
								
									
										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