diff --git a/backend/Cargo.lock b/backend/Cargo.lock
index 0f5a358..ee4af06 100644
--- a/backend/Cargo.lock
+++ b/backend/Cargo.lock
@@ -1893,6 +1893,7 @@ dependencies = [
"dotenvy",
"memory",
"tokio",
+ "utils",
]
[[package]]
@@ -1908,6 +1909,7 @@ dependencies = [
"postgres",
"sqlx",
"tokio",
+ "utils",
]
[[package]]
@@ -1921,6 +1923,7 @@ dependencies = [
"domain",
"memory",
"tokio",
+ "utils",
]
[[package]]
@@ -1936,6 +1939,7 @@ dependencies = [
"postgres",
"sqlx",
"tokio",
+ "utils",
]
[[package]]
@@ -1948,6 +1952,7 @@ dependencies = [
"memory",
"tokio",
"tui",
+ "utils",
]
[[package]]
@@ -1962,6 +1967,7 @@ dependencies = [
"sqlx",
"tokio",
"tui",
+ "utils",
]
[[package]]
@@ -2633,6 +2639,7 @@ dependencies = [
"ratatui",
"textwrap",
"tokio",
+ "uuid",
]
[[package]]
@@ -2733,6 +2740,16 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+[[package]]
+name = "utils"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "domain",
+ "dotenvy",
+ "tempfile",
+]
+
[[package]]
name = "uuid"
version = "1.17.0"
diff --git a/backend/Cargo.toml b/backend/Cargo.toml
index 5f3c057..919f8a9 100644
--- a/backend/Cargo.toml
+++ b/backend/Cargo.toml
@@ -15,7 +15,7 @@ tokio = { version = "1.36", features = ["full"] }
anyhow = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
-uuid = { version = "1.7", features = ["v4", "serde"] }
+uuid = { version = "1.8", features = ["v4", "v7", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
thiserror = "1.0"
clap = { version = "4.5", features = ["derive"] }
@@ -29,6 +29,7 @@ dotenvy = "0.15"
ratatui = "0.24"
crossterm = "0.27"
textwrap = "0.16"
+tempfile = "3.10"
[profile.release]
lto = true
diff --git a/backend/config/api-memory.env b/backend/config/api-memory.env
index cbee19e..36a77f3 100644
--- a/backend/config/api-memory.env
+++ b/backend/config/api-memory.env
@@ -2,5 +2,8 @@
HOST=127.0.0.1
PORT=3001
+# Hub Universe DID Configuration
+HUB_UNIVERSE_DID=u:Test:018f1234-5678-9abc-def0-123456789012
+
# Optional: Logging Configuration
-RUST_LOG=info
\ No newline at end of file
+RUST_LOG=info
\ No newline at end of file
diff --git a/backend/config/api-postgres.env b/backend/config/api-postgres.env
index 8e15556..62d74af 100644
--- a/backend/config/api-postgres.env
+++ b/backend/config/api-postgres.env
@@ -5,5 +5,8 @@ PORT=3001
# Database Configuration
DATABASE_URL=postgres://postgres:password@localhost:5432/sharenet
+# Hub Universe DID Configuration
+HUB_UNIVERSE_DID=u:Test:018f1234-5678-9abc-def0-123456789012
+
# Optional: Logging Configuration
-RUST_LOG=info
\ No newline at end of file
+RUST_LOG=info
\ No newline at end of file
diff --git a/backend/config/cli-memory.env b/backend/config/cli-memory.env
index d7a9434..8514a83 100644
--- a/backend/config/cli-memory.env
+++ b/backend/config/cli-memory.env
@@ -1,2 +1,5 @@
# Optional: Logging Configuration
-RUST_LOG=info
\ No newline at end of file
+RUST_LOG=info
+
+# Hub Universe DID Configuration
+HUB_UNIVERSE_DID=u:Test:018f1234-5678-9abc-def0-123456789012
\ No newline at end of file
diff --git a/backend/config/cli-postgres.env b/backend/config/cli-postgres.env
index bc69fb3..7a2135d 100644
--- a/backend/config/cli-postgres.env
+++ b/backend/config/cli-postgres.env
@@ -2,4 +2,7 @@
DATABASE_URL=postgres://postgres:password@localhost:5432/sharenet
# Optional: Logging Configuration
-RUST_LOG=info
\ No newline at end of file
+RUST_LOG=info
+
+# Hub Universe DID Configuration
+HUB_UNIVERSE_DID=u:Test:018f1234-5678-9abc-def0-123456789012
\ No newline at end of file
diff --git a/backend/config/tui-memory.env b/backend/config/tui-memory.env
index 4cfeff6..7bd32b0 100644
--- a/backend/config/tui-memory.env
+++ b/backend/config/tui-memory.env
@@ -1,2 +1,5 @@
# Memory TUI Configuration
-RUST_LOG=info
\ No newline at end of file
+RUST_LOG=info
+
+# Hub Universe DID Configuration
+HUB_UNIVERSE_DID=u:Test:018f1234-5678-9abc-def0-123456789012
\ No newline at end of file
diff --git a/backend/config/tui-postgres.env b/backend/config/tui-postgres.env
index 39319a6..e3cdf80 100644
--- a/backend/config/tui-postgres.env
+++ b/backend/config/tui-postgres.env
@@ -1,3 +1,6 @@
# Postgres TUI Configuration
RUST_LOG=info
-DATABASE_URL=postgres://postgres:password@localhost:5432/sharenet
\ No newline at end of file
+DATABASE_URL=postgres://postgres:password@localhost:5432/sharenet
+
+# Hub Universe DID Configuration
+HUB_UNIVERSE_DID=u:Test:018f1234-5678-9abc-def0-123456789012
\ No newline at end of file
diff --git a/backend/crates/api/src/lib.rs b/backend/crates/api/src/lib.rs
index 3377a7d..b8a555a 100644
--- a/backend/crates/api/src/lib.rs
+++ b/backend/crates/api/src/lib.rs
@@ -108,6 +108,7 @@ use uuid::Uuid;
pub struct AppState {
user_service: Arc,
product_service: Arc
,
+ hub_universe_did: String,
}
impl Clone for AppState
@@ -119,6 +120,7 @@ where
Self {
user_service: self.user_service.clone(),
product_service: self.product_service.clone(),
+ hub_universe_did: self.hub_universe_did.clone(),
}
}
}
@@ -133,9 +135,10 @@ where
/// * `addr` - The socket address to bind the server to
/// * `user_service` - Service implementation for user operations
/// * `product_service` - Service implementation for product operations
+/// * `hub_universe_did` - The hub universe DID for passport affiliation
///
/// See the module-level documentation for usage examples.
-pub async fn run(addr: SocketAddr, user_service: U, product_service: P)
+pub async fn run(addr: SocketAddr, user_service: U, product_service: P, hub_universe_did: String)
where
U: UseCase + Clone + Send + Sync + 'static,
P: UseCase + Clone + Send + Sync + 'static,
@@ -150,6 +153,7 @@ where
let state = AppState {
user_service: Arc::new(user_service),
product_service: Arc::new(product_service),
+ hub_universe_did,
};
// Configure CORS
@@ -160,6 +164,7 @@ where
let app = Router::new()
.route("/health", get(health_check))
+ .route("/hub/universe-did", get(get_universe_did::))
.route("/users", post(create_user::))
.route("/users/:id", get(get_user::))
.route("/users", get(list_users::))
@@ -390,6 +395,22 @@ async fn health_check() -> impl IntoResponse {
})))
}
+/// Get hub universe DID endpoint
+///
+/// Returns the hub universe DID for passport affiliation.
+/// This endpoint is used by the frontend to get the hub's universe DID
+/// when creating new passports.
+///
+/// # Response
+/// - `200 OK` - Universe DID returned successfully
+async fn get_universe_did(
+ State(state): State>,
+) -> impl IntoResponse {
+ (StatusCode::OK, Json(serde_json::json!({
+ "did": state.hub_universe_did
+ })))
+}
+
#[cfg(test)]
mod tests {
//! # API Tests
@@ -582,14 +603,16 @@ mod tests {
fn create_test_app() -> Router {
let user_service = MockUserService::new();
let product_service = MockProductService::new();
-
+
let state = AppState {
user_service: Arc::new(user_service),
product_service: Arc::new(product_service),
+ hub_universe_did: "u:hub:12345678-1234-1234-1234-123456789012".to_string(),
};
Router::new()
.route("/health", get(health_check))
+ .route("/hub/universe-did", get(get_universe_did::))
.route("/users", post(create_user::))
.route("/users/:id", get(get_user::))
.route("/users", get(list_users::))
@@ -995,7 +1018,7 @@ mod tests {
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
-
+
let health_data: serde_json::Value = extract_json(response).await;
assert_eq!(health_data["status"], "healthy");
assert_eq!(health_data["service"], "sharenet-api");
@@ -1003,6 +1026,61 @@ mod tests {
}
}
+ mod universe_did_endpoint {
+ //! # Universe DID Endpoint Tests
+ //!
+ //! Tests for the hub universe DID endpoint used by the frontend
+ //! to get the hub's universe DID for passport affiliation.
+
+ use super::*;
+
+ /// Tests the universe DID endpoint returns the configured DID.
+ #[tokio::test]
+ async fn test_get_universe_did() {
+ let app = create_test_app();
+
+ let response = app
+ .oneshot(
+ Request::builder()
+ .method("GET")
+ .uri("/hub/universe-did")
+ .body(Body::empty())
+ .unwrap(),
+ )
+ .await
+ .unwrap();
+
+ assert_eq!(response.status(), StatusCode::OK);
+
+ let did_data: serde_json::Value = extract_json(response).await;
+ assert_eq!(did_data["did"], "u:hub:12345678-1234-1234-1234-123456789012");
+ }
+
+ /// Tests that the universe DID endpoint returns a valid JSON structure.
+ #[tokio::test]
+ async fn test_universe_did_response_structure() {
+ let app = create_test_app();
+
+ let response = app
+ .oneshot(
+ Request::builder()
+ .method("GET")
+ .uri("/hub/universe-did")
+ .body(Body::empty())
+ .unwrap(),
+ )
+ .await
+ .unwrap();
+
+ assert_eq!(response.status(), StatusCode::OK);
+
+ let did_data: serde_json::Value = extract_json(response).await;
+ assert!(did_data.is_object());
+ assert!(did_data.get("did").is_some());
+ assert!(did_data["did"].is_string());
+ }
+ }
+
mod product_endpoints {
//! # Product Endpoint Tests
//!
diff --git a/backend/crates/cli/src/lib.rs b/backend/crates/cli/src/lib.rs
index daa1bd2..623fd12 100644
--- a/backend/crates/cli/src/lib.rs
+++ b/backend/crates/cli/src/lib.rs
@@ -11,7 +11,7 @@
use anyhow::Result;
use clap::Parser;
-use domain::{CreateProduct, CreateUser, Product, User, UpdateProduct, UpdateUser};
+use domain::{CreateProduct, CreateUser, Product, User, UpdateProduct, UpdateUser, UniverseDid};
use application::UseCase;
use uuid::Uuid;
@@ -34,6 +34,11 @@ pub enum Commands {
#[command(subcommand)]
command: ProductCommands,
},
+ /// Universe DID management commands
+ UniverseDid {
+ #[command(subcommand)]
+ command: UniverseDidCommands,
+ },
}
#[derive(Parser)]
@@ -114,8 +119,28 @@ pub enum ProductCommands {
},
}
+#[derive(Parser)]
+pub enum UniverseDidCommands {
+ /// Create a new universe DID
+ Create {
+ /// Universe name
+ #[arg(short, long)]
+ name: String,
+ },
+ /// Validate a universe DID
+ Validate {
+ /// Universe DID to validate
+ #[arg(short, long)]
+ did: String,
+ },
+ /// Generate a default hub universe DID
+ GenerateHub,
+ /// Get the hub universe DID
+ GetHub,
+}
+
impl Cli {
- pub async fn run(self, user_service: U, product_service: P) -> Result<()>
+ pub async fn run(self, user_service: U, product_service: P, hub_universe_did: String) -> Result<()>
where
U: UseCase,
P: UseCase,
@@ -167,6 +192,54 @@ impl Cli {
println!("Deleted product {}", id);
}
},
+ Some(Commands::UniverseDid { command }) => match command {
+ UniverseDidCommands::Create { name } => {
+ let universe_did = UniverseDid::new(format!("u:{}:{}", name, uuid::Uuid::now_v7()))?;
+ println!("Created universe DID: {}", universe_did.did());
+ }
+ UniverseDidCommands::Validate { did } => {
+ match UniverseDid::new(did.clone()) {
+ Ok(universe_did) => {
+ println!("Valid universe DID: {}", universe_did.did());
+ if let Some(name) = universe_did.name() {
+ println!(" Name: {}", name);
+ }
+ if let Some(uuid) = universe_did.uuid() {
+ println!(" UUID: {}", uuid);
+ }
+ }
+ Err(e) => {
+ println!("Invalid universe DID '{}': {}", did, e);
+ }
+ }
+ }
+ UniverseDidCommands::GenerateHub => {
+ let hub_did = UniverseDid::default_hub();
+ println!("Generated hub universe DID: {}", hub_did.did());
+ if let Some(name) = hub_did.name() {
+ println!(" Name: {}", name);
+ }
+ if let Some(uuid) = hub_did.uuid() {
+ println!(" UUID: {}", uuid);
+ }
+ }
+ UniverseDidCommands::GetHub => {
+ println!("Hub universe DID: {}", hub_universe_did);
+ match UniverseDid::new(hub_universe_did.clone()) {
+ Ok(universe_did) => {
+ if let Some(name) = universe_did.name() {
+ println!(" Name: {}", name);
+ }
+ if let Some(uuid) = universe_did.uuid() {
+ println!(" UUID: {}", uuid);
+ }
+ }
+ Err(e) => {
+ println!("Warning: Hub universe DID is invalid: {}", e);
+ }
+ }
+ }
+ },
None => {
println!("No command provided. Use --help for usage information.");
}
diff --git a/backend/crates/domain/src/lib.rs b/backend/crates/domain/src/lib.rs
index df889e5..d00c776 100644
--- a/backend/crates/domain/src/lib.rs
+++ b/backend/crates/domain/src/lib.rs
@@ -838,3 +838,181 @@ mod tests {
}
}
}
+
+// Hub Universe DID domain model
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct UniverseDid {
+ did: String,
+}
+
+impl UniverseDid {
+ pub fn new(did: String) -> Result {
+ if did.trim().is_empty() {
+ return Err(DomainError::InvalidInput("DID cannot be empty".to_string()));
+ }
+
+ // Universe DID format validation: u:name:uuidv7
+ let parts: Vec<&str> = did.split(':').collect();
+ if parts.len() != 3 || parts[0] != "u" {
+ return Err(DomainError::InvalidInput(
+ "Universe DID must follow format: u:name:uuidv7".to_string()
+ ));
+ }
+
+ // Validate UUIDv7 format
+ if uuid::Uuid::parse_str(parts[2]).is_err() {
+ return Err(DomainError::InvalidInput(
+ "Invalid UUIDv7 format in Universe DID".to_string()
+ ));
+ }
+
+ Ok(Self { did })
+ }
+
+ // Constructor for generating a default hub universe DID
+ pub fn default_hub() -> Self {
+ Self {
+ did: format!("u:hub:{}", uuid::Uuid::now_v7()),
+ }
+ }
+
+ // Getters
+ pub fn did(&self) -> &str {
+ &self.did
+ }
+
+ // Get the name part of the universe DID
+ pub fn name(&self) -> Option<&str> {
+ let parts: Vec<&str> = self.did.split(':').collect();
+ if parts.len() == 3 {
+ Some(parts[1])
+ } else {
+ None
+ }
+ }
+
+ // Get the UUID part of the universe DID
+ pub fn uuid(&self) -> Option {
+ let parts: Vec<&str> = self.did.split(':').collect();
+ if parts.len() == 3 {
+ uuid::Uuid::parse_str(parts[2]).ok()
+ } else {
+ None
+ }
+ }
+}
+
+impl Entity for UniverseDid {
+ type Create = ();
+ type Update = ();
+}
+
+#[cfg(test)]
+mod universe_did_tests {
+ use super::*;
+
+ #[test]
+ fn test_universe_did_valid_format() {
+ let valid_did = format!("u:test:{}", uuid::Uuid::now_v7());
+ let universe_did = UniverseDid::new(valid_did.clone()).unwrap();
+
+ assert_eq!(universe_did.did(), valid_did);
+ assert_eq!(universe_did.name(), Some("test"));
+ assert!(universe_did.uuid().is_some());
+ }
+
+ #[test]
+ fn test_universe_did_invalid_format() {
+ // Missing 'u' prefix
+ let result = UniverseDid::new("did:test:12345678-1234-1234-1234-123456789012".to_string());
+ assert!(result.is_err());
+ assert!(matches!(result.unwrap_err(), DomainError::InvalidInput(_)));
+
+ // Wrong number of parts
+ let result = UniverseDid::new("u:test".to_string());
+ assert!(result.is_err());
+ assert!(matches!(result.unwrap_err(), DomainError::InvalidInput(_)));
+
+ // Invalid UUID
+ let result = UniverseDid::new("u:test:invalid-uuid".to_string());
+ assert!(result.is_err());
+ assert!(matches!(result.unwrap_err(), DomainError::InvalidInput(_)));
+
+ // Empty DID
+ let result = UniverseDid::new("".to_string());
+ assert!(result.is_err());
+ assert!(matches!(result.unwrap_err(), DomainError::InvalidInput(_)));
+ }
+
+ #[test]
+ fn test_universe_did_default_hub() {
+ let hub_did = UniverseDid::default_hub();
+
+ assert!(hub_did.did().starts_with("u:hub:"));
+ assert_eq!(hub_did.name(), Some("hub"));
+ assert!(hub_did.uuid().is_some());
+ }
+
+ #[test]
+ fn test_universe_did_name_extraction() {
+ let universe_did = UniverseDid::new("u:myuniverse:12345678-1234-1234-1234-123456789012".to_string()).unwrap();
+
+ assert_eq!(universe_did.name(), Some("myuniverse"));
+ }
+
+ #[test]
+ fn test_universe_did_uuid_extraction() {
+ let uuid = uuid::Uuid::now_v7();
+ let universe_did = UniverseDid::new(format!("u:test:{}", uuid)).unwrap();
+
+ assert_eq!(universe_did.uuid(), Some(uuid));
+ }
+
+ #[test]
+ fn test_universe_did_validation_edge_cases() {
+ // Test empty string
+ let result = UniverseDid::new("".to_string());
+ assert!(result.is_err());
+
+ // Test whitespace only
+ let result = UniverseDid::new(" ".to_string());
+ assert!(result.is_err());
+
+ // Test missing 'u' prefix
+ let result = UniverseDid::new("did:test:12345678-1234-1234-1234-123456789012".to_string());
+ assert!(result.is_err());
+
+ // Test wrong number of parts (too few)
+ let result = UniverseDid::new("u:test".to_string());
+ assert!(result.is_err());
+
+ // Test wrong number of parts (too many)
+ let result = UniverseDid::new("u:test:uuid:extra".to_string());
+ assert!(result.is_err());
+
+ // Test invalid UUID format
+ let result = UniverseDid::new("u:test:not-a-uuid".to_string());
+ assert!(result.is_err());
+
+ // Test valid format with different names
+ let valid_did = format!("u:myuniverse:{}", uuid::Uuid::now_v7());
+ let result = UniverseDid::new(valid_did.clone());
+ assert!(result.is_ok());
+ assert_eq!(result.unwrap().did(), valid_did);
+ }
+
+ #[test]
+ fn test_universe_did_name_and_uuid_extraction_edge_cases() {
+ // Test with complex name
+ let universe_did = UniverseDid::new("u:my-complex-universe-name:12345678-1234-1234-1234-123456789012".to_string()).unwrap();
+ assert_eq!(universe_did.name(), Some("my-complex-universe-name"));
+
+ // Test with numbers in name
+ let universe_did = UniverseDid::new("u:universe123:12345678-1234-1234-1234-123456789012".to_string()).unwrap();
+ assert_eq!(universe_did.name(), Some("universe123"));
+
+ // Test with underscores in name
+ let universe_did = UniverseDid::new("u:my_universe:12345678-1234-1234-1234-123456789012".to_string()).unwrap();
+ assert_eq!(universe_did.name(), Some("my_universe"));
+ }
+}
diff --git a/backend/crates/sharenet-api-memory/Cargo.toml b/backend/crates/sharenet-api-memory/Cargo.toml
index 04b35ed..3addb1c 100644
--- a/backend/crates/sharenet-api-memory/Cargo.toml
+++ b/backend/crates/sharenet-api-memory/Cargo.toml
@@ -15,3 +15,4 @@ clap = { workspace = true, features = ["derive"] }
tokio = { workspace = true, features = ["full", "macros", "rt-multi-thread"] }
anyhow = { workspace = true }
dotenvy = { workspace = true }
+utils = { path = "../utils" }
diff --git a/backend/crates/sharenet-api-memory/src/main.rs b/backend/crates/sharenet-api-memory/src/main.rs
index f29056a..2fe8ae0 100644
--- a/backend/crates/sharenet-api-memory/src/main.rs
+++ b/backend/crates/sharenet-api-memory/src/main.rs
@@ -6,11 +6,12 @@ use application::Service;
use domain::{User, Product};
use memory::{InMemoryProductRepository, InMemoryUserRepository};
use std::env;
+use utils::HubConfig;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
- // Load environment variables from config file
- dotenvy::from_path("config/api-memory.env").ok();
+ // Load and validate hub universe DID from environment file
+ let hub_universe_did = HubConfig::read_default_hub_universe_did_from_file("config/api-memory.env")?;
// Get configuration from environment variables
let host = env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
@@ -27,7 +28,7 @@ async fn main() -> anyhow::Result<()> {
// Run API server
let addr = format!("{}:{}", host, port);
let addr = SocketAddr::from_str(&addr)?;
- run_api(addr, user_service, product_service).await;
+ run_api(addr, user_service, product_service, hub_universe_did.did().to_string()).await;
Ok(())
}
\ No newline at end of file
diff --git a/backend/crates/sharenet-api-postgres/Cargo.toml b/backend/crates/sharenet-api-postgres/Cargo.toml
index 9b81847..8b22813 100644
--- a/backend/crates/sharenet-api-postgres/Cargo.toml
+++ b/backend/crates/sharenet-api-postgres/Cargo.toml
@@ -14,4 +14,5 @@ clap = { workspace = true, features = ["derive"] }
tokio = { workspace = true, features = ["full", "macros", "rt-multi-thread"] }
anyhow = { workspace = true }
sqlx = { workspace = true }
-dotenvy = { workspace = true }
\ No newline at end of file
+dotenvy = { workspace = true }
+utils = { path = "../utils" }
\ No newline at end of file
diff --git a/backend/crates/sharenet-api-postgres/src/main.rs b/backend/crates/sharenet-api-postgres/src/main.rs
index f6adb92..8ecd48d 100644
--- a/backend/crates/sharenet-api-postgres/src/main.rs
+++ b/backend/crates/sharenet-api-postgres/src/main.rs
@@ -6,13 +6,13 @@ use application::Service;
use domain::{User, Product};
use postgres::{PostgresProductRepository, PostgresUserRepository};
use sqlx::postgres::PgPoolOptions;
-use dotenvy;
use std::env;
+use utils::HubConfig;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
- // Load environment variables from config file
- dotenvy::from_path("config/api-postgres.env").ok();
+ // Load and validate hub universe DID from environment file
+ let hub_universe_did = HubConfig::read_default_hub_universe_did_from_file("config/api-postgres.env")?;
// Get configuration from environment variables
let host = env::var("HOST").unwrap_or_else(|_| "0.0.0.0".to_string());
@@ -41,7 +41,7 @@ async fn main() -> anyhow::Result<()> {
// Run API server
let addr = format!("{}:{}", host, port);
let addr = SocketAddr::from_str(&addr)?;
- run_api(addr, user_service, product_service).await;
+ run_api(addr, user_service, product_service, hub_universe_did.did().to_string()).await;
Ok(())
}
\ No newline at end of file
diff --git a/backend/crates/sharenet-cli-memory/Cargo.toml b/backend/crates/sharenet-cli-memory/Cargo.toml
index a6eb509..a4bd01b 100644
--- a/backend/crates/sharenet-cli-memory/Cargo.toml
+++ b/backend/crates/sharenet-cli-memory/Cargo.toml
@@ -12,4 +12,5 @@ application = { path = "../application" }
cli = { path = "../cli" }
memory = { path = "../memory" }
tokio = { workspace = true }
-clap = { workspace = true }
\ No newline at end of file
+clap = { workspace = true }
+utils = { path = "../utils" }
\ No newline at end of file
diff --git a/backend/crates/sharenet-cli-memory/src/main.rs b/backend/crates/sharenet-cli-memory/src/main.rs
index e8818ff..b539329 100644
--- a/backend/crates/sharenet-cli-memory/src/main.rs
+++ b/backend/crates/sharenet-cli-memory/src/main.rs
@@ -2,12 +2,16 @@ use anyhow::Result;
use clap::Parser;
use memory::{MemoryUserService, MemoryProductService};
use cli::Cli;
+use utils::HubConfig;
#[tokio::main]
async fn main() -> Result<()> {
+ // Load and validate hub universe DID from environment file
+ let hub_universe_did = HubConfig::read_default_hub_universe_did_from_file("config/cli-memory.env")?;
+
let user_service = MemoryUserService::new(memory::InMemoryUserRepository::new());
let product_service = MemoryProductService::new(memory::InMemoryProductRepository::new());
-
+
let cli = Cli::try_parse()?;
- cli.run(user_service, product_service).await
+ cli.run(user_service, product_service, hub_universe_did.did().to_string()).await
}
\ No newline at end of file
diff --git a/backend/crates/sharenet-cli-postgres/Cargo.toml b/backend/crates/sharenet-cli-postgres/Cargo.toml
index 4d6875a..8182ea0 100644
--- a/backend/crates/sharenet-cli-postgres/Cargo.toml
+++ b/backend/crates/sharenet-cli-postgres/Cargo.toml
@@ -14,4 +14,5 @@ postgres = { path = "../postgres" }
tokio = { workspace = true }
clap = { workspace = true }
dotenvy = { workspace = true }
-sqlx = { workspace = true }
\ No newline at end of file
+sqlx = { workspace = true }
+utils = { path = "../utils" }
\ No newline at end of file
diff --git a/backend/crates/sharenet-cli-postgres/src/main.rs b/backend/crates/sharenet-cli-postgres/src/main.rs
index 3e275cf..c536ac1 100644
--- a/backend/crates/sharenet-cli-postgres/src/main.rs
+++ b/backend/crates/sharenet-cli-postgres/src/main.rs
@@ -2,14 +2,19 @@ use anyhow::Result;
use clap::Parser;
use postgres::{PostgresUserService, PostgresProductService};
use cli::Cli;
+use utils::HubConfig;
#[tokio::main]
async fn main() -> Result<()> {
dotenvy::from_path("config/cli-postgres.env").ok();
+
+ // Load and validate hub universe DID from environment file
+ let hub_universe_did = HubConfig::read_default_hub_universe_did_from_file("config/cli-postgres.env")?;
+
let pool = sqlx::PgPool::connect(&std::env::var("DATABASE_URL")?).await?;
let user_service = PostgresUserService::new(postgres::PostgresUserRepository::new(pool.clone()));
let product_service = PostgresProductService::new(postgres::PostgresProductRepository::new(pool));
-
+
let cli = Cli::try_parse()?;
- cli.run(user_service, product_service).await
+ cli.run(user_service, product_service, hub_universe_did.did().to_string()).await
}
\ No newline at end of file
diff --git a/backend/crates/sharenet-tui-memory/Cargo.toml b/backend/crates/sharenet-tui-memory/Cargo.toml
index a056d55..1ad9673 100644
--- a/backend/crates/sharenet-tui-memory/Cargo.toml
+++ b/backend/crates/sharenet-tui-memory/Cargo.toml
@@ -12,3 +12,4 @@ application = { path = "../application" }
tui = { path = "../tui" }
memory = { path = "../memory" }
tokio = { workspace = true }
+utils = { path = "../utils" }
diff --git a/backend/crates/sharenet-tui-memory/src/main.rs b/backend/crates/sharenet-tui-memory/src/main.rs
index 58fb7af..fa92609 100644
--- a/backend/crates/sharenet-tui-memory/src/main.rs
+++ b/backend/crates/sharenet-tui-memory/src/main.rs
@@ -1,11 +1,15 @@
use anyhow::Result;
use memory::{MemoryUserService, MemoryProductService};
use tui::run_tui;
+use utils::HubConfig;
#[tokio::main]
async fn main() -> Result<()> {
+ // Load and validate hub universe DID from environment file
+ let hub_universe_did = HubConfig::read_default_hub_universe_did_from_file("config/tui-memory.env")?;
+
let user_service = MemoryUserService::new(memory::InMemoryUserRepository::new());
let product_service = MemoryProductService::new(memory::InMemoryProductRepository::new());
-
- run_tui(user_service, product_service).await
+
+ run_tui(user_service, product_service, hub_universe_did.did().to_string()).await
}
diff --git a/backend/crates/sharenet-tui-postgres/Cargo.toml b/backend/crates/sharenet-tui-postgres/Cargo.toml
index 104bbc6..e00b3ea 100644
--- a/backend/crates/sharenet-tui-postgres/Cargo.toml
+++ b/backend/crates/sharenet-tui-postgres/Cargo.toml
@@ -14,3 +14,4 @@ postgres = { path = "../postgres" }
tokio = { workspace = true }
dotenvy = { workspace = true }
sqlx = { workspace = true }
+utils = { path = "../utils" }
diff --git a/backend/crates/sharenet-tui-postgres/src/main.rs b/backend/crates/sharenet-tui-postgres/src/main.rs
index 4744820..ccbc4d6 100644
--- a/backend/crates/sharenet-tui-postgres/src/main.rs
+++ b/backend/crates/sharenet-tui-postgres/src/main.rs
@@ -1,6 +1,7 @@
use anyhow::Result;
use postgres::{PostgresUserService, PostgresProductService};
use tui::run_tui;
+use utils::HubConfig;
#[tokio::main]
async fn main() -> Result<()> {
@@ -36,8 +37,11 @@ async fn main() -> Result<()> {
Err(e) => println!("DATABASE_URL not found: {}", e),
}
+ // Load and validate hub universe DID from environment file
+ let hub_universe_did = HubConfig::read_default_hub_universe_did_from_file("config/tui-postgres.env")?;
+
let pool = sqlx::PgPool::connect(&std::env::var("DATABASE_URL")?).await?;
let user_service = PostgresUserService::new(postgres::PostgresUserRepository::new(pool.clone()));
let product_service = PostgresProductService::new(postgres::PostgresProductRepository::new(pool));
- run_tui(user_service, product_service).await
+ run_tui(user_service, product_service, hub_universe_did.did().to_string()).await
}
diff --git a/backend/crates/tui/Cargo.toml b/backend/crates/tui/Cargo.toml
index acded88..e3621d7 100644
--- a/backend/crates/tui/Cargo.toml
+++ b/backend/crates/tui/Cargo.toml
@@ -13,4 +13,5 @@ memory = { path = "../memory" }
ratatui = { workspace = true }
crossterm = { workspace = true }
textwrap = "0.16"
-tokio = { workspace = true }
\ No newline at end of file
+tokio = { workspace = true }
+uuid = { workspace = true }
\ No newline at end of file
diff --git a/backend/crates/tui/src/lib.rs b/backend/crates/tui/src/lib.rs
index 9a3b2db..1bad6c1 100644
--- a/backend/crates/tui/src/lib.rs
+++ b/backend/crates/tui/src/lib.rs
@@ -21,7 +21,7 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
-use domain::{CreateProduct, CreateUser, Product, User};
+use domain::{CreateProduct, CreateUser, Product, User, UniverseDid};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
@@ -142,7 +142,7 @@ impl App {
}
}
-pub async fn run_tui(user_service: U, product_service: P) -> anyhow::Result<()>
+pub async fn run_tui(user_service: U, product_service: P, hub_universe_did: String) -> anyhow::Result<()>
where
U: UseCase + Clone + Send + 'static,
P: UseCase + Clone + Send + 'static,
@@ -156,7 +156,7 @@ where
// Create app and run it
let app = App::new();
- let res = run_app(&mut terminal, app, user_service, product_service).await;
+ let res = run_app(&mut terminal, app, user_service, product_service, hub_universe_did).await;
// Restore terminal
disable_raw_mode()?;
@@ -179,6 +179,7 @@ async fn run_app(
mut app: App,
user_service: U,
product_service: P,
+ hub_universe_did: String,
) -> anyhow::Result<()>
where
U: UseCase + Clone + Send + 'static,
@@ -307,6 +308,62 @@ where
Err(e) => app.add_message(format!("Error: {}", e)),
}
}
+ cmd if cmd.starts_with("universe-did create") => {
+ match parse_universe_did_create(cmd) {
+ Ok(name) => {
+ match UniverseDid::new(format!("u:{}:{}", name, uuid::Uuid::now_v7())) {
+ Ok(universe_did) => app.add_message(format!("Created universe DID: {}", universe_did.did())),
+ Err(e) => app.add_message(format!("Error: {}", e)),
+ }
+ }
+ Err(e) => app.add_message(format!("Error: {}", e)),
+ }
+ }
+ cmd if cmd.starts_with("universe-did validate") => {
+ match parse_universe_did_validate(cmd) {
+ Ok(did) => {
+ match UniverseDid::new(did.clone()) {
+ Ok(universe_did) => {
+ let mut message = format!("Valid universe DID: {}", universe_did.did());
+ if let Some(name) = universe_did.name() {
+ message.push_str(&format!("\n Name: {}", name));
+ }
+ if let Some(uuid) = universe_did.uuid() {
+ message.push_str(&format!("\n UUID: {}", uuid));
+ }
+ app.add_message(message);
+ }
+ Err(e) => app.add_message(format!("Invalid universe DID '{}': {}", did, e)),
+ }
+ }
+ Err(e) => app.add_message(format!("Error: {}", e)),
+ }
+ }
+ "universe-did generate-hub" => {
+ let hub_did = UniverseDid::default_hub();
+ let mut message = format!("Generated hub universe DID: {}", hub_did.did());
+ if let Some(name) = hub_did.name() {
+ message.push_str(&format!("\n Name: {}", name));
+ }
+ if let Some(uuid) = hub_did.uuid() {
+ message.push_str(&format!("\n UUID: {}", uuid));
+ }
+ app.add_message(message);
+ }
+ "universe-did get-hub" => {
+ app.add_message(format!("Hub universe DID: {}", hub_universe_did));
+ match UniverseDid::new(hub_universe_did.clone()) {
+ Ok(universe_did) => {
+ if let Some(name) = universe_did.name() {
+ app.add_message(format!(" Name: {}", name));
+ }
+ if let Some(uuid) = universe_did.uuid() {
+ app.add_message(format!(" UUID: {}", uuid));
+ }
+ }
+ Err(e) => app.add_message(format!("Warning: Hub universe DID is invalid: {}", e)),
+ }
+ }
"" => {}
_ => {
app.add_message("Unknown command. Type 'help' for available commands.".to_string());
@@ -334,6 +391,12 @@ fn print_help(app: &mut App) {
app.add_message(" product create -n -d ".to_string());
app.add_message(" Example: product create -n \"My Product\" -d \"A great product description\"".to_string());
app.add_message(" product list".to_string());
+ app.add_message(" universe-did create -n ".to_string());
+ app.add_message(" Example: universe-did create -n myuniverse".to_string());
+ app.add_message(" universe-did validate -d ".to_string());
+ app.add_message(" Example: universe-did validate -d u:myuniverse:12345678-1234-1234-1234-123456789012".to_string());
+ app.add_message(" universe-did generate-hub".to_string());
+ app.add_message(" universe-did get-hub".to_string());
app.add_message("\nTips:".to_string());
app.add_message(" - Use quotes for values with spaces".to_string());
app.add_message(" - Use Up/Down arrows to navigate command history".to_string());
@@ -532,6 +595,110 @@ fn parse_product_create(cmd: &str) -> anyhow::Result<(String, String)> {
}
}
+fn parse_universe_did_create(cmd: &str) -> anyhow::Result {
+ let parts: Vec<&str> = cmd.split_whitespace().collect();
+ if parts.len() < 4 {
+ return Err(anyhow::anyhow!(
+ "Invalid command format. Use: universe-did create -n \nExample: universe-did create -n myuniverse"
+ ));
+ }
+
+ let mut name = None;
+ let mut current_arg = None;
+ let mut current_value = Vec::new();
+
+ // Skip "universe-did create" command
+ let mut i = 2;
+ while i < parts.len() {
+ match parts[i] {
+ "-n" => {
+ if let Some(arg_type) = current_arg {
+ match arg_type {
+ "name" => name = Some(current_value.join(" ")),
+ _ => {}
+ }
+ }
+ current_arg = Some("name");
+ current_value.clear();
+ i += 1;
+ }
+ _ => {
+ if current_arg.is_some() {
+ current_value.push(parts[i].trim_matches('"'));
+ }
+ i += 1;
+ }
+ }
+ }
+
+ // Handle the last argument
+ if let Some(arg_type) = current_arg {
+ match arg_type {
+ "name" => name = Some(current_value.join(" ")),
+ _ => {}
+ }
+ }
+
+ match name {
+ Some(n) if !n.is_empty() => Ok(n),
+ _ => Err(anyhow::anyhow!(
+ "Invalid command format. Use: universe-did create -n \nExample: universe-did create -n myuniverse"
+ )),
+ }
+}
+
+fn parse_universe_did_validate(cmd: &str) -> anyhow::Result {
+ let parts: Vec<&str> = cmd.split_whitespace().collect();
+ if parts.len() < 4 {
+ return Err(anyhow::anyhow!(
+ "Invalid command format. Use: universe-did validate -d \nExample: universe-did validate -d u:myuniverse:12345678-1234-1234-1234-123456789012"
+ ));
+ }
+
+ let mut did = None;
+ let mut current_arg = None;
+ let mut current_value = Vec::new();
+
+ // Skip "universe-did validate" command
+ let mut i = 2;
+ while i < parts.len() {
+ match parts[i] {
+ "-d" => {
+ if let Some(arg_type) = current_arg {
+ match arg_type {
+ "did" => did = Some(current_value.join(" ")),
+ _ => {}
+ }
+ }
+ current_arg = Some("did");
+ current_value.clear();
+ i += 1;
+ }
+ _ => {
+ if current_arg.is_some() {
+ current_value.push(parts[i].trim_matches('"'));
+ }
+ i += 1;
+ }
+ }
+ }
+
+ // Handle the last argument
+ if let Some(arg_type) = current_arg {
+ match arg_type {
+ "did" => did = Some(current_value.join(" ")),
+ _ => {}
+ }
+ }
+
+ match did {
+ Some(d) if !d.is_empty() => Ok(d),
+ _ => Err(anyhow::anyhow!(
+ "Invalid command format. Use: universe-did validate -d \nExample: universe-did validate -d u:myuniverse:12345678-1234-1234-1234-123456789012"
+ )),
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/backend/crates/utils/Cargo.toml b/backend/crates/utils/Cargo.toml
new file mode 100644
index 0000000..924b307
--- /dev/null
+++ b/backend/crates/utils/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "utils"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+license.workspace = true
+
+[dependencies]
+anyhow.workspace = true
+domain = { path = "../domain" }
+dotenvy.workspace = true
+
+[dev-dependencies]
+tempfile.workspace = true
\ No newline at end of file
diff --git a/backend/crates/utils/src/lib.rs b/backend/crates/utils/src/lib.rs
new file mode 100644
index 0000000..8ae4902
--- /dev/null
+++ b/backend/crates/utils/src/lib.rs
@@ -0,0 +1,129 @@
+/*
+ * This file is part of Sharenet.
+ *
+ * Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
+ *
+ * You may obtain a copy of the license at:
+ * https://creativecommons.org/licenses/by-nc-sa/4.0/
+ *
+ * Copyright (c) 2024 Continuist
+ */
+
+use anyhow::Result;
+use domain::UniverseDid;
+use std::env;
+
+/// Configuration utility for reading and validating hub universe DID from environment
+pub struct HubConfig;
+
+impl HubConfig {
+ /// Read and validate hub universe DID from environment variable
+ ///
+ /// # Arguments
+ /// * `env_var` - The environment variable name to read from
+ ///
+ /// # Returns
+ /// * `Result` - Validated universe DID or error
+ pub fn read_hub_universe_did(env_var: &str) -> Result {
+ let hub_universe_did = env::var(env_var)
+ .map_err(|_| anyhow::anyhow!("{} must be set", env_var))?;
+
+ let universe_did = UniverseDid::new(hub_universe_did.clone())
+ .map_err(|e| anyhow::anyhow!("Invalid {} format: {}", env_var, e))?;
+
+ Ok(universe_did)
+ }
+
+ /// Read hub universe DID with default environment variable name "HUB_UNIVERSE_DID"
+ pub fn read_default_hub_universe_did() -> Result {
+ Self::read_hub_universe_did("HUB_UNIVERSE_DID")
+ }
+
+ /// Read hub universe DID from a specific environment file
+ ///
+ /// # Arguments
+ /// * `env_file_path` - Path to the environment file
+ /// * `env_var` - The environment variable name to read
+ ///
+ /// # Returns
+ /// * `Result` - Validated universe DID or error
+ pub fn read_hub_universe_did_from_file(env_file_path: &str, env_var: &str) -> Result {
+ // Load environment variables from the specified file
+ dotenvy::from_path(env_file_path)
+ .map_err(|e| anyhow::anyhow!("Failed to load environment file {}: {}", env_file_path, e))?;
+
+ Self::read_hub_universe_did(env_var)
+ }
+
+ /// Read hub universe DID from default environment file with default variable name
+ pub fn read_default_hub_universe_did_from_file(env_file_path: &str) -> Result {
+ Self::read_hub_universe_did_from_file(env_file_path, "HUB_UNIVERSE_DID")
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::env;
+ use tempfile::NamedTempFile;
+ use std::io::Write;
+
+ #[test]
+ fn test_read_hub_universe_did_valid() {
+ let valid_did = "u:hub:12345678-1234-1234-1234-123456789012";
+ env::set_var("TEST_HUB_DID", valid_did);
+
+ let result = HubConfig::read_hub_universe_did("TEST_HUB_DID");
+ assert!(result.is_ok());
+ assert_eq!(result.unwrap().did(), valid_did);
+
+ env::remove_var("TEST_HUB_DID");
+ }
+
+ #[test]
+ fn test_read_hub_universe_did_missing() {
+ env::remove_var("TEST_MISSING_DID");
+
+ let result = HubConfig::read_hub_universe_did("TEST_MISSING_DID");
+ assert!(result.is_err());
+ assert!(result.unwrap_err().to_string().contains("must be set"));
+ }
+
+ #[test]
+ fn test_read_hub_universe_did_invalid_format() {
+ env::set_var("TEST_INVALID_DID", "invalid-format");
+
+ let result = HubConfig::read_hub_universe_did("TEST_INVALID_DID");
+ assert!(result.is_err());
+ assert!(result.unwrap_err().to_string().contains("Invalid format"));
+
+ env::remove_var("TEST_INVALID_DID");
+ }
+
+ #[test]
+ fn test_read_from_file_valid() {
+ let mut temp_file = NamedTempFile::new().unwrap();
+ writeln!(temp_file, "HUB_UNIVERSE_DID=u:hub:12345678-1234-1234-1234-123456789012").unwrap();
+
+ let result = HubConfig::read_hub_universe_did_from_file(temp_file.path().to_str().unwrap(), "HUB_UNIVERSE_DID");
+ assert!(result.is_ok());
+ assert_eq!(result.unwrap().did(), "u:hub:12345678-1234-1234-1234-123456789012");
+ }
+
+ #[test]
+ fn test_read_from_file_missing_file() {
+ let result = HubConfig::read_hub_universe_did_from_file("/nonexistent/file.env", "HUB_UNIVERSE_DID");
+ assert!(result.is_err());
+ assert!(result.unwrap_err().to_string().contains("Failed to load environment file"));
+ }
+
+ #[test]
+ fn test_read_from_file_missing_var() {
+ let mut temp_file = NamedTempFile::new().unwrap();
+ writeln!(temp_file, "OTHER_VAR=value").unwrap();
+
+ let result = HubConfig::read_hub_universe_did_from_file(temp_file.path().to_str().unwrap(), "HUB_UNIVERSE_DID");
+ assert!(result.is_err());
+ assert!(result.unwrap_err().to_string().contains("must be set"));
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/components/auth/create-passport-dialog.tsx b/frontend/src/components/auth/create-passport-dialog.tsx
new file mode 100644
index 0000000..e095833
--- /dev/null
+++ b/frontend/src/components/auth/create-passport-dialog.tsx
@@ -0,0 +1,291 @@
+'use client';
+
+import React, { useState, useEffect } from 'react';
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
+import { Input } from '@/components/ui/input';
+import { Button } from '@/components/ui/button';
+import { hubApi } from '@/lib/api';
+import { PassportBrowserIO } from '@/lib/wasm-browser';
+import type { UserIdentity, UserPreferences } from '@/lib/auth/types';
+
+interface CreatePassportDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ onPassportCreated?: () => void;
+}
+
+export function CreatePassportDialog({ open, onOpenChange, onPassportCreated }: CreatePassportDialogProps) {
+ const [universeDID, setUniverseDID] = useState('');
+ const [isLoadingUniverseDID, setIsLoadingUniverseDID] = useState(false);
+ const [useHubUniverseDID, setUseHubUniverseDID] = useState(true);
+ const [isCreating, setIsCreating] = useState(false);
+ const [password, setPassword] = useState('');
+ const [confirmPassword, setConfirmPassword] = useState('');
+ const [recoveryPassphrase, setRecoveryPassphrase] = useState('');
+ const [showPassphrase, setShowPassphrase] = useState(false);
+
+
+ // Load hub universe DID when dialog opens
+ useEffect(() => {
+ if (open) {
+ loadUniverseDID();
+ }
+ }, [open]);
+
+ const loadUniverseDID = async () => {
+ setIsLoadingUniverseDID(true);
+ try {
+ const response = await hubApi.getUniverseDID();
+ setUniverseDID(response.data.did);
+ } catch (error) {
+ console.error('Failed to load universe DID:', error);
+ setUniverseDID('');
+ } finally {
+ setIsLoadingUniverseDID(false);
+ }
+ };
+
+
+ const handleCreatePassport = async () => {
+ if (!password) {
+ alert('Password is required');
+ return;
+ }
+
+ if (password !== confirmPassword) {
+ alert('Passwords do not match');
+ return;
+ }
+
+ setIsCreating(true);
+ try {
+ // TODO: Implement actual .spf file generation using WASM
+ // For now, we'll create a mock implementation
+ await generateAndDownloadPassport();
+
+ // Don't reset form or close dialog here - let the user see the recovery passphrase first
+ // The form reset and dialog close will happen when the user clicks "I've Saved My Passphrase"
+ } catch (error) {
+ console.error('Failed to create passport:', error);
+ alert('Failed to create passport. Please try again.');
+ } finally {
+ setIsCreating(false);
+ }
+ };
+
+ const generateAndDownloadPassport = async () => {
+ try {
+ // Get the universe ID
+ const univId = useHubUniverseDID ? universeDID : "did:example:custom-universe";
+
+ // Create passport using browser I/O operations
+ // This creates a basic passport with a default user profile
+ const result = await PassportBrowserIO.createPassport(
+ univId,
+ password
+ );
+
+ // Set the recovery passphrase for display
+ setRecoveryPassphrase(result.recoveryPhrase);
+
+ // Download the passport file
+ const fileName = 'passport.spf';
+ const url = URL.createObjectURL(result.downloadBlob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = fileName;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+
+ setShowPassphrase(true);
+ } catch (error) {
+ console.error('Failed to create passport:', error);
+ throw new Error(`Failed to create passport: ${error instanceof Error ? error.message : 'Unknown error'}`);
+ }
+ };
+
+
+ const handleClose = () => {
+ onOpenChange(false);
+ };
+
+ const handlePassphraseAcknowledged = () => {
+ // Reset form
+ setPassword('');
+ setConfirmPassword('');
+ setRecoveryPassphrase('');
+ setShowPassphrase(false);
+
+ onOpenChange(false);
+ onPassportCreated?.();
+ };
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/components/auth/login-button.tsx b/frontend/src/components/auth/login-button.tsx
index fb71184..882da9f 100644
--- a/frontend/src/components/auth/login-button.tsx
+++ b/frontend/src/components/auth/login-button.tsx
@@ -3,6 +3,9 @@
import React, { useState } from 'react';
import { Button } from '@/components/ui/button';
import { PassportFilePicker } from './passport-file-picker';
+import { CreatePassportDialog } from './create-passport-dialog';
+import { ProfileManagementDialog } from './profile-management-dialog';
+import { useAuth } from '@/lib/auth/context';
interface LoginButtonProps {
className?: string;
@@ -10,12 +13,28 @@ interface LoginButtonProps {
export function LoginButton({ className }: LoginButtonProps) {
const [showFilePicker, setShowFilePicker] = useState(false);
+ const [showCreatePassport, setShowCreatePassport] = useState(false);
+ const [showProfileManagement, setShowProfileManagement] = useState(false);
+ const [selectedPassportFile, setSelectedPassportFile] = useState(null);
- const handleFileSelected = (file: File) => {
+ const handleFileSelected = async (file: File) => {
console.log('File selected:', file.name);
+ setSelectedPassportFile(file);
+ // The passport-file-picker will handle password input and decryption
+ // We just need to close the file picker dialog
setShowFilePicker(false);
};
+ const handleCreatePassport = () => {
+ setShowFilePicker(false);
+ setShowCreatePassport(true);
+ };
+
+ const handlePassportCreated = () => {
+ // Optional: Show success message or trigger login with the new passport
+ console.log('Passport created successfully');
+ };
+
return (
<>