first commit

This commit is contained in:
Timur Gordon
2025-09-24 05:11:15 +02:00
commit be061409af
19 changed files with 6052 additions and 0 deletions

45
components/Cargo.toml Normal file
View File

@@ -0,0 +1,45 @@
[package]
name = "self-components"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
yew = { workspace = true }
wasm-bindgen = { workspace = true }
wasm-bindgen-futures = { workspace = true }
web-sys = { workspace = true, features = [
"console",
"HtmlInputElement",
"HtmlTextAreaElement",
"Event",
"EventTarget",
"InputEvent",
"MouseEvent",
"Window",
"Document",
"Element",
"EventSource",
"MessageEvent",
"Clipboard",
"Navigator",
"Crypto",
"CryptoKey",
"SubtleCrypto",
"AesKeyGenParams",
"CryptoKeyPair",
] }
js-sys = { workspace = true }
gloo = { workspace = true }
gloo-timers = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
getrandom = { workspace = true }
sha2 = { workspace = true }
aes-gcm = { workspace = true }
base64 = { workspace = true }
hex = { workspace = true }
rand = { workspace = true }
k256 = { version = "0.13", features = ["ecdsa", "sha256"] }

158
components/src/crypto.rs Normal file
View File

@@ -0,0 +1,158 @@
use wasm_bindgen::prelude::*;
use web_sys::console;
use serde::{Deserialize, Serialize};
use sha2::{Sha256, Digest};
use aes_gcm::{Aes256Gcm, Key, Nonce, aead::{Aead, KeyInit}};
use rand::RngCore;
use base64::{Engine as _, engine::general_purpose};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyPair {
pub private_key: String,
pub public_key: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncryptedPrivateKey {
pub encrypted_data: String,
pub nonce: String,
pub salt: String,
}
/// Generate a new secp256k1 key pair
pub fn generate_keypair() -> Result<KeyPair, String> {
// Generate 32 random bytes for private key
let mut private_key_bytes = [0u8; 32];
getrandom::getrandom(&mut private_key_bytes)
.map_err(|e| format!("Failed to generate random bytes: {:?}", e))?;
// Ensure private key is valid (not zero, not greater than curve order)
// For simplicity, we'll just ensure it's not all zeros
if private_key_bytes.iter().all(|&b| b == 0) {
return Err("Generated invalid private key".to_string());
}
let private_key = hex::encode(private_key_bytes);
// Generate public key from private key (simplified secp256k1 derivation)
let public_key = derive_public_key(&private_key)?;
Ok(KeyPair {
private_key,
public_key,
})
}
/// Derive public key from private key (simplified implementation)
fn derive_public_key(private_key: &str) -> Result<String, String> {
let private_bytes = hex::decode(private_key)
.map_err(|e| format!("Invalid private key hex: {:?}", e))?;
// Simple hash-based public key derivation (not actual secp256k1)
// In production, use proper secp256k1 point multiplication
let mut hasher = Sha256::new();
hasher.update(&private_bytes);
hasher.update(b"secp256k1_public_key");
let hash = hasher.finalize();
// Add uncompressed public key prefix
let mut public_key = vec![0x04];
public_key.extend_from_slice(&hash);
// Add another hash to make it 65 bytes total (uncompressed public key size)
let mut hasher2 = Sha256::new();
hasher2.update(&hash);
let hash2 = hasher2.finalize();
public_key.extend_from_slice(&hash2);
Ok(hex::encode(public_key))
}
/// Encrypt private key with password using AES-256-GCM
pub fn encrypt_private_key(private_key: &str, password: &str) -> Result<EncryptedPrivateKey, String> {
// Generate random salt
let mut salt = [0u8; 32];
getrandom::getrandom(&mut salt)
.map_err(|e| format!("Failed to generate salt: {:?}", e))?;
// Derive key from password using PBKDF2-like approach (simplified)
let mut hasher = Sha256::new();
hasher.update(password.as_bytes());
hasher.update(&salt);
// Multiple rounds for key stretching
let mut key_material = hasher.finalize().to_vec();
for _ in 0..10000 {
let mut hasher = Sha256::new();
hasher.update(&key_material);
hasher.update(&salt);
key_material = hasher.finalize().to_vec();
}
let key = Key::<Aes256Gcm>::from_slice(&key_material);
let cipher = Aes256Gcm::new(key);
// Generate random nonce
let mut nonce_bytes = [0u8; 12];
getrandom::getrandom(&mut nonce_bytes)
.map_err(|e| format!("Failed to generate nonce: {:?}", e))?;
let nonce = Nonce::from_slice(&nonce_bytes);
// Encrypt private key
let ciphertext = cipher.encrypt(nonce, private_key.as_bytes())
.map_err(|e| format!("Encryption failed: {:?}", e))?;
Ok(EncryptedPrivateKey {
encrypted_data: general_purpose::STANDARD.encode(&ciphertext),
nonce: general_purpose::STANDARD.encode(&nonce_bytes),
salt: general_purpose::STANDARD.encode(&salt),
})
}
/// Decrypt private key with password
pub fn decrypt_private_key(encrypted: &EncryptedPrivateKey, password: &str) -> Result<String, String> {
let salt = general_purpose::STANDARD.decode(&encrypted.salt)
.map_err(|e| format!("Invalid salt base64: {:?}", e))?;
let nonce_bytes = general_purpose::STANDARD.decode(&encrypted.nonce)
.map_err(|e| format!("Invalid nonce base64: {:?}", e))?;
let ciphertext = general_purpose::STANDARD.decode(&encrypted.encrypted_data)
.map_err(|e| format!("Invalid encrypted data base64: {:?}", e))?;
// Derive key from password (same as encryption)
let mut hasher = Sha256::new();
hasher.update(password.as_bytes());
hasher.update(&salt);
let mut key_material = hasher.finalize().to_vec();
for _ in 0..10000 {
let mut hasher = Sha256::new();
hasher.update(&key_material);
hasher.update(&salt);
key_material = hasher.finalize().to_vec();
}
let key = Key::<Aes256Gcm>::from_slice(&key_material);
let cipher = Aes256Gcm::new(key);
let nonce = Nonce::from_slice(&nonce_bytes);
// Decrypt
let plaintext = cipher.decrypt(nonce, ciphertext.as_ref())
.map_err(|e| format!("Decryption failed: {:?}", e))?;
String::from_utf8(plaintext)
.map_err(|e| format!("Invalid UTF-8 in decrypted data: {:?}", e))
}
/// Copy text to clipboard
pub async fn copy_to_clipboard(text: &str) -> Result<(), String> {
let window = web_sys::window().ok_or("No window object")?;
let navigator = window.navigator();
let clipboard = navigator.clipboard();
let promise = clipboard.write_text(text);
wasm_bindgen_futures::JsFuture::from(promise)
.await
.map_err(|_| {
// Fallback: show alert with text to copy manually
let _ = window.alert_with_message(&format!("Copy this text: {}", text));
"Failed to copy to clipboard".to_string()
})?;
Ok(())
}

5
components/src/lib.rs Normal file
View File

@@ -0,0 +1,5 @@
pub mod registration;
pub mod crypto;
pub use registration::{Registration, RegistrationConfig};
pub use crypto::*;

View File

@@ -0,0 +1,780 @@
use yew::prelude::*;
use web_sys::{HtmlInputElement, EventSource, MessageEvent};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use serde::{Deserialize, Serialize};
use gloo_timers::callback::Timeout;
use crate::crypto::{generate_keypair, encrypt_private_key, copy_to_clipboard, KeyPair, EncryptedPrivateKey};
use k256::{SecretKey, PublicKey};
use k256::elliptic_curve::sec1::ToEncodedPoint;
use rand::rngs::OsRng;
use hex;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct RegistrationConfig {
pub server_url: String,
pub app_name: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum EmailVerificationStatus {
NotStarted,
Pending,
Verified,
Failed,
}
#[derive(Debug, Clone, PartialEq)]
pub enum RegistrationStep {
Identity,
EmailVerification,
KeyGeneration,
KeyConfirmation,
Complete,
}
#[derive(Properties, PartialEq)]
pub struct RegistrationProps {
pub config: RegistrationConfig,
pub on_complete: Callback<(String, String)>, // (email, public_key)
}
pub struct Registration {
// Form data
name: String,
email: String,
// Key management
keypair: Option<KeyPair>,
secret_phrase: String,
generated_private_key: Option<String>,
generated_public_key: Option<String>,
private_key_input: String,
key_copied: bool,
key_confirmation: String,
// Email verification
email_status: EmailVerificationStatus,
event_source: Option<EventSource>,
// UI state
current_step: RegistrationStep,
show_private_key: bool,
errors: Vec<String>,
processing: bool,
}
pub enum RegistrationMsg {
UpdateName(String),
UpdateEmail(String),
UpdateSecretPhrase(String),
UpdatePrivateKeyInput(String),
SendEmailVerification,
EmailVerified,
EmailVerificationFailed,
GenerateKeys,
UpdateKeyConfirmation(String),
TogglePrivateKeyVisibility,
CopyPrivateKey,
NextStep,
SubmitRegistration,
RegistrationComplete,
RegistrationFailed(String),
ClearErrors,
}
impl Component for Registration {
type Message = RegistrationMsg;
type Properties = RegistrationProps;
fn create(_ctx: &Context<Self>) -> Self {
Self {
name: String::new(),
email: String::new(),
keypair: None,
secret_phrase: String::new(),
generated_private_key: None,
generated_public_key: None,
private_key_input: String::new(),
key_copied: false,
key_confirmation: String::new(),
email_status: EmailVerificationStatus::NotStarted,
event_source: None,
current_step: RegistrationStep::Identity,
show_private_key: false,
errors: Vec::new(),
processing: false,
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
RegistrationMsg::UpdateName(name) => {
self.name = name;
true
}
RegistrationMsg::UpdateEmail(email) => {
self.email = email;
if self.email_status == EmailVerificationStatus::Verified {
self.email_status = EmailVerificationStatus::NotStarted;
}
true
}
RegistrationMsg::UpdateSecretPhrase(secret) => {
self.secret_phrase = secret;
true
}
RegistrationMsg::UpdatePrivateKeyInput(value) => {
self.private_key_input = value;
true
}
RegistrationMsg::UpdateKeyConfirmation(value) => {
self.key_confirmation = value;
true
}
RegistrationMsg::SendEmailVerification => {
self.send_email_verification(ctx);
true
}
RegistrationMsg::EmailVerified => {
self.email_status = EmailVerificationStatus::Verified;
if let Some(event_source) = &self.event_source {
event_source.close();
self.event_source = None;
}
true
}
RegistrationMsg::EmailVerificationFailed => {
self.email_status = EmailVerificationStatus::Failed;
if let Some(event_source) = &self.event_source {
event_source.close();
self.event_source = None;
}
true
}
RegistrationMsg::GenerateKeys => {
self.generate_secp256k1_keys();
true
}
RegistrationMsg::CopyPrivateKey => {
self.copy_private_key();
false
}
RegistrationMsg::TogglePrivateKeyVisibility => {
self.show_private_key = !self.show_private_key;
true
}
RegistrationMsg::NextStep => {
// No longer needed - single step form
true
}
RegistrationMsg::SubmitRegistration => {
if self.validate_complete_form() {
self.submit_registration(ctx);
}
true
}
RegistrationMsg::RegistrationComplete => {
self.current_step = RegistrationStep::Complete;
if let Some(keypair) = &self.keypair {
ctx.props().on_complete.emit((self.email.clone(), keypair.public_key.clone()));
}
true
}
RegistrationMsg::RegistrationFailed(error) => {
self.processing = false;
self.errors.push(error);
true
}
RegistrationMsg::ClearErrors => {
self.errors.clear();
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
if self.current_step == RegistrationStep::Complete {
return self.render_complete_step();
}
html! {
<div class="registration-container" style="max-width: 600px; margin: 0 auto; padding: 2rem;">
<div class="card shadow-lg border-0" style="border-radius: 16px;">
<div class="card-header bg-primary text-white text-center" style="border-radius: 16px 16px 0 0; padding: 2rem;">
<h2 class="mb-0">{"Self-Sovereign Identity"}</h2>
<p class="mb-0 mt-2 opacity-75">{"Create your decentralized identity"}</p>
</div>
<div class="card-body" style="padding: 2rem;">
{self.render_errors(ctx)}
{self.render_identity_step(ctx)}
</div>
</div>
</div>
}
}
}
impl Registration {
fn render_errors(&self, ctx: &Context<Self>) -> Html {
if self.errors.is_empty() {
return html! {};
}
let link = ctx.link();
html! {
<div class="alert alert-danger alert-dismissible fade show mb-4">
<ul class="mb-0">
{for self.errors.iter().map(|error| html! {
<li>{error}</li>
})}
</ul>
<button type="button" class="btn-close"
onclick={link.callback(|_| RegistrationMsg::ClearErrors)}></button>
</div>
}
}
fn render_status_notification(&self) -> Html {
let (alert_class, icon, message) = if self.processing {
("alert-info", "bi-hourglass-split", "Processing registration...")
} else if self.name.trim().is_empty() {
("alert-secondary", "bi-person", "Please enter your full name to get started")
} else if self.email.trim().is_empty() {
("alert-secondary", "bi-envelope", "Please enter your email address")
} else if self.email_status == EmailVerificationStatus::NotStarted {
("alert-warning", "bi-envelope-exclamation", "Please verify your email address")
} else if self.email_status == EmailVerificationStatus::Pending {
("alert-info", "bi-envelope-check", "Check your email and click the verification link")
} else if self.email_status == EmailVerificationStatus::Failed {
("alert-danger", "bi-envelope-x", "Email verification failed. Please try again")
} else if self.secret_phrase.trim().is_empty() {
("alert-warning", "bi-key", "Please enter a secret phrase to generate your keys")
} else if self.generated_private_key.is_none() {
("alert-warning", "bi-key-fill", "Please generate your cryptographic keys")
} else if self.key_confirmation.is_empty() {
("alert-warning", "bi-shield-exclamation", "Please confirm your private key to complete registration. Save it securely - it cannot be recovered if lost!")
} else if self.generated_private_key.as_ref() != Some(&self.key_confirmation) {
("alert-danger", "bi-shield-x", "Private key confirmation does not match. Please try again")
} else {
("alert-success", "bi-shield-check", "All requirements completed! Ready to register")
};
html! {
<div class={format!("alert {} mb-3", alert_class)}>
<i class={format!("bi {} me-2", icon)}></i>
{message}
</div>
}
}
fn render_identity_step(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
html! {
<div class="step-content">
<h4 class="mb-4">{"Personal Information"}</h4>
<div class="mb-3">
<label class="form-label">{"Full Name"}</label>
<input type="text" class="form-control"
value={self.name.clone()}
oninput={link.callback(|e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
RegistrationMsg::UpdateName(input.value())
})}
placeholder="Enter your full name" />
</div>
<div class="mb-4">
<label class="form-label">{"Email Address"}</label>
<div class="d-flex align-items-center gap-2">
<input type="email" class="form-control"
value={self.email.clone()}
oninput={link.callback(|e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
RegistrationMsg::UpdateEmail(input.value())
})}
placeholder="your.email@example.com" />
{match self.email_status {
EmailVerificationStatus::NotStarted => html! {
<button type="button" class="btn btn-outline-primary"
onclick={link.callback(|_| RegistrationMsg::SendEmailVerification)}
disabled={self.email.trim().is_empty()}>
{"Verify"}
</button>
},
EmailVerificationStatus::Pending => html! {
<div class="text-warning d-flex align-items-center">
<i class="bi bi-clock-history me-1"></i>
{"Verifying..."}
</div>
},
EmailVerificationStatus::Verified => html! {
<div class="text-success d-flex align-items-center">
<i class="bi bi-check-circle-fill me-1"></i>
{"Verified"}
</div>
},
EmailVerificationStatus::Failed => html! {
<button type="button" class="btn btn-outline-danger"
onclick={link.callback(|_| RegistrationMsg::SendEmailVerification)}>
{"Retry"}
</button>
},
}}
</div>
</div>
<div class="mb-4">
<label class="form-label">{"Secret Phrase for Key Generation"}</label>
<div class="d-flex align-items-center gap-2">
<input type="password" class="form-control"
value={self.secret_phrase.clone()}
oninput={link.callback(|e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
RegistrationMsg::UpdateSecretPhrase(input.value())
})}
placeholder="Enter a secret phrase to generate your keys" />
<button type="button" class="btn btn-outline-primary"
onclick={link.callback(|_| RegistrationMsg::GenerateKeys)}
disabled={self.secret_phrase.trim().is_empty()}>
<i class="bi bi-key me-1"></i>
{"Generate"}
</button>
</div>
</div>
<div class="mb-4">
<div class="d-flex justify-content-between align-items-center mb-2">
<label class="form-label mb-0">{"Generated Private Key"}</label>
<div>
<button type="button" class="btn btn-sm btn-outline-secondary me-2"
onclick={link.callback(|_| RegistrationMsg::TogglePrivateKeyVisibility)}
disabled={self.generated_private_key.is_none()}>
{if self.show_private_key { "Hide" } else { "Show" }}
</button>
<button type="button" class="btn btn-sm btn-primary"
onclick={link.callback(|_| RegistrationMsg::CopyPrivateKey)}
disabled={self.generated_private_key.is_none()}>
<i class="bi bi-copy me-1"></i>
{"Copy"}
</button>
</div>
</div>
<div class="bg-dark text-light p-3 rounded font-monospace small"
style="word-break: break-all;">
{if let Some(private_key) = &self.generated_private_key {
if self.show_private_key {
private_key.clone()
} else {
"••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••".to_string()
}
} else {
"Private key will appear here after generation".to_string()
}}
</div>
</div>
{if let Some(_) = &self.generated_private_key {
html! {
<div class="mb-4">
<label class="form-label">{"Confirm Private Key"}</label>
<input type="password" class="form-control"
value={self.key_confirmation.clone()}
oninput={link.callback(|e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
RegistrationMsg::UpdateKeyConfirmation(input.value())
})}
placeholder="Enter your private key to confirm" />
</div>
}
} else {
html! {}
}}
{self.render_status_notification()}
<div class="d-grid">
<button type="button" class="btn btn-primary btn-lg"
onclick={link.callback(|_| RegistrationMsg::SubmitRegistration)}
disabled={!self.validate_complete_form()}>
{if self.processing {
html! {
<>
<span class="spinner-border spinner-border-sm me-2"></span>
{"Registering..."}
</>
}
} else {
html! { "Complete Registration" }
}}
</button>
</div>
</div>
}
}
fn render_email_verification_step(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
html! {
<div class="step-content">
<h4 class="mb-4">{"Email Verification"}</h4>
<div class="mb-4">
<div class="d-flex align-items-center">
<input type="email" class="form-control me-3"
value={self.email.clone()} readonly=true />
{match self.email_status {
EmailVerificationStatus::NotStarted => html! {
<button type="button" class="btn btn-outline-primary"
onclick={link.callback(|_| RegistrationMsg::SendEmailVerification)}>
{"Send Verification"}
</button>
},
EmailVerificationStatus::Pending => html! {
<div class="text-warning">
<i class="bi bi-clock-history me-2"></i>
{"Waiting..."}
</div>
},
EmailVerificationStatus::Verified => html! {
<div class="text-success">
<i class="bi bi-check-circle-fill me-2"></i>
{"Verified"}
</div>
},
EmailVerificationStatus::Failed => html! {
<div class="text-danger">
<i class="bi bi-x-circle-fill me-2"></i>
{"Failed"}
</div>
},
}}
</div>
</div>
<div class="d-grid">
<button type="button" class="btn btn-primary"
onclick={link.callback(|_| RegistrationMsg::NextStep)}
disabled={self.email_status != EmailVerificationStatus::Verified}>
{"Continue"}
</button>
</div>
</div>
}
}
fn render_key_generation_step(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
html! {
<div class="step-content">
<h4 class="mb-4">{"Generate Secp256k1 Keys"}</h4>
<div class="mb-3">
<label class="form-label">{"Secret Phrase"}</label>
<input type="password" class="form-control"
value={self.secret_phrase.clone()}
oninput={link.callback(|e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
RegistrationMsg::UpdateSecretPhrase(input.value())
})}
placeholder="Enter a secret phrase to generate your keys" />
</div>
<div class="mb-4">
<button type="button" class="btn btn-primary"
onclick={link.callback(|_| RegistrationMsg::GenerateKeys)}
disabled={self.secret_phrase.trim().is_empty()}>
<i class="bi bi-key me-2"></i>
{"Generate Keys"}
</button>
</div>
{if let Some(private_key) = &self.generated_private_key {
html! {
<div class="mb-4">
<div class="d-flex justify-content-between align-items-center mb-2">
<label class="form-label mb-0">{"Generated Private Key"}</label>
<div>
<button type="button" class="btn btn-sm btn-outline-secondary me-2"
onclick={link.callback(|_| RegistrationMsg::TogglePrivateKeyVisibility)}>
{if self.show_private_key { "Hide" } else { "Show" }}
</button>
<button type="button" class="btn btn-sm btn-primary"
onclick={link.callback(|_| RegistrationMsg::CopyPrivateKey)}>
<i class="bi bi-copy me-1"></i>
{"Copy"}
</button>
</div>
</div>
<div class="bg-dark text-light p-3 rounded font-monospace small"
style="word-break: break-all;">
{if self.show_private_key {
private_key.clone()
} else {
"••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••".to_string()
}}
</div>
</div>
}
} else {
html! {}
}}
</div>
}
}
fn render_key_confirmation_step(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
html! {
<div class="step-content">
<h4 class="mb-4">{"Confirm Private Key"}</h4>
<p class="mb-4">{"Please enter your private key to confirm you have saved it securely:"}</p>
<div class="mb-4">
<div class="d-flex align-items-center gap-2">
<input type="password" class="form-control font-monospace"
value={self.private_key_input.clone()}
oninput={link.callback(|e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
RegistrationMsg::UpdatePrivateKeyInput(input.value())
})}
placeholder="Enter your private key..." />
{if !self.private_key_input.is_empty() {
let is_correct = self.generated_private_key.as_ref()
.map(|pk| pk == &self.private_key_input.trim())
.unwrap_or(false);
if is_correct {
html! {
<button type="button" class="btn btn-success"
onclick={link.callback(|_| RegistrationMsg::SubmitRegistration)}
disabled={self.processing}>
{if self.processing {
html! {
<>
<span class="spinner-border spinner-border-sm me-2"></span>
{"Registering..."}
</>
}
} else {
html! { "Register" }
}}
</button>
}
} else {
html! {
<button type="button" class="btn btn-outline-danger" disabled=true>
{"Invalid Key"}
</button>
}
}
} else {
html! {
<button type="button" class="btn btn-outline-secondary" disabled=true>
{"Register"}
</button>
}
}}
</div>
{if !self.private_key_input.is_empty() {
let is_correct = self.generated_private_key.as_ref()
.map(|pk| pk == &self.private_key_input.trim())
.unwrap_or(false);
if is_correct {
html! {
<div class="alert alert-success mt-2">
<i class="bi bi-check-circle me-2"></i>
{"Private key confirmed successfully!"}
</div>
}
} else {
html! {
<div class="alert alert-danger mt-2">
<i class="bi bi-x-circle me-2"></i>
{"Private key does not match. Please try again."}
</div>
}
}
} else {
html! {}
}}
</div>
</div>
}
}
fn render_complete_step(&self) -> Html {
html! {
<div class="step-content text-center">
<div class="mb-4">
<i class="bi bi-check-circle-fill text-success" style="font-size: 4rem;"></i>
</div>
<h4 class="mb-3">{"Registration Complete!"}</h4>
<p class="text-muted">{"Your self-sovereign identity has been created successfully."}</p>
<div class="alert alert-info">
<strong>{"Email:"}</strong> {&self.email}<br/>
<strong>{"Public Key:"}</strong>
<code class="d-block mt-2 small text-break">
{self.generated_public_key.as_ref().unwrap_or(&String::new())}
</code>
</div>
</div>
}
}
fn validate_complete_form(&self) -> bool {
!self.name.trim().is_empty() &&
!self.email.trim().is_empty() &&
self.email_status == EmailVerificationStatus::Verified &&
self.generated_private_key.is_some() &&
if let Some(private_key) = &self.generated_private_key {
self.key_confirmation == *private_key
} else {
false
}
}
fn send_email_verification(&mut self, ctx: &Context<Self>) {
self.email_status = EmailVerificationStatus::Pending;
let server_url = ctx.props().config.server_url.clone();
let email = self.email.clone();
let link = ctx.link().clone();
// Send verification request
wasm_bindgen_futures::spawn_local(async move {
let url = format!("{}/api/send-verification", server_url);
let body = serde_json::json!({
"email": email
});
// Make HTTP request to server
let mut opts = web_sys::RequestInit::new();
opts.method("POST");
opts.mode(web_sys::RequestMode::Cors);
let headers = web_sys::Headers::new().unwrap();
headers.set("Content-Type", "application/json").unwrap();
opts.headers(&headers);
opts.body(Some(&wasm_bindgen::JsValue::from_str(&body.to_string())));
let request = web_sys::Request::new_with_str_and_init(&url, &opts).unwrap();
let window = web_sys::window().unwrap();
match wasm_bindgen_futures::JsFuture::from(window.fetch_with_request(&request)).await {
Ok(_response) => {
web_sys::console::log_1(&format!("Email verification sent to: {}", email).into());
}
Err(e) => {
web_sys::console::error_1(&format!("Failed to send verification: {:?}", e).into());
}
}
// Set up SSE connection for verification status
let sse_url = format!("{}/api/verification-status/{}", server_url, email);
match EventSource::new(&sse_url) {
Ok(event_source) => {
let link_clone = link.clone();
let onmessage_callback = Closure::wrap(Box::new(move |e: MessageEvent| {
if let Some(data) = e.data().as_string() {
if data == "verified" {
link_clone.send_message(RegistrationMsg::EmailVerified);
} else if data == "failed" {
link_clone.send_message(RegistrationMsg::EmailVerificationFailed);
}
}
}) as Box<dyn FnMut(_)>);
event_source.set_onmessage(Some(onmessage_callback.as_ref().unchecked_ref()));
onmessage_callback.forget();
}
Err(_) => {
link.send_message(RegistrationMsg::EmailVerificationFailed);
}
}
});
}
fn generate_secp256k1_keys(&mut self) {
use sha2::{Sha256, Digest};
// Use secret phrase to derive private key deterministically
let mut hasher = Sha256::new();
hasher.update(self.secret_phrase.as_bytes());
let hash = hasher.finalize();
// Generate secp256k1 keypair from hash
match SecretKey::from_slice(&hash) {
Ok(secret_key) => {
let public_key = secret_key.public_key();
// Store keys as hex strings
self.generated_private_key = Some(hex::encode(secret_key.to_bytes()));
self.generated_public_key = Some(hex::encode(public_key.to_encoded_point(false).as_bytes()));
}
Err(_) => {
self.errors.push("Failed to generate valid private key from secret phrase".to_string());
}
}
}
fn copy_private_key(&mut self) {
if let Some(private_key) = &self.generated_private_key {
// Use web API to copy to clipboard
if let Some(window) = web_sys::window() {
let navigator = window.navigator().clipboard();
let _ = navigator.write_text(private_key);
self.key_copied = true;
}
}
}
fn submit_registration(&mut self, ctx: &Context<Self>) {
self.processing = true;
let server_url = ctx.props().config.server_url.clone();
let email = self.email.clone();
let public_key = self.generated_public_key.as_ref().unwrap().clone();
let link = ctx.link().clone();
wasm_bindgen_futures::spawn_local(async move {
let _url = format!("{}/api/register", server_url);
let _body = serde_json::json!({
"email": email,
"public_key": public_key
});
// In a real implementation, make HTTP request here
web_sys::console::log_1(&format!("Registering: {} with key: {}", email, public_key).into());
// Simulate API call
let timeout = Timeout::new(2000, move || {
link.send_message(RegistrationMsg::RegistrationComplete);
});
timeout.forget();
});
}
}