Add remaining files for feature
Some checks failed
Podman Rootless Demo / test-backend (push) Has been skipped
Podman Rootless Demo / test-frontend (push) Has been skipped
Podman Rootless Demo / build-backend (push) Has been skipped
Podman Rootless Demo / build-frontend (push) Failing after 9m0s
Podman Rootless Demo / deploy-prod (push) Has been skipped
Podman Rootless Demo / test-backend (pull_request) Has been skipped
Podman Rootless Demo / test-frontend (pull_request) Has been skipped
Podman Rootless Demo / build-backend (pull_request) Has been skipped
Podman Rootless Demo / build-frontend (pull_request) Failing after 8m29s
Podman Rootless Demo / deploy-prod (pull_request) Has been skipped
Some checks failed
Podman Rootless Demo / test-backend (push) Has been skipped
Podman Rootless Demo / test-frontend (push) Has been skipped
Podman Rootless Demo / build-backend (push) Has been skipped
Podman Rootless Demo / build-frontend (push) Failing after 9m0s
Podman Rootless Demo / deploy-prod (push) Has been skipped
Podman Rootless Demo / test-backend (pull_request) Has been skipped
Podman Rootless Demo / test-frontend (pull_request) Has been skipped
Podman Rootless Demo / build-backend (pull_request) Has been skipped
Podman Rootless Demo / build-frontend (pull_request) Failing after 8m29s
Podman Rootless Demo / deploy-prod (pull_request) Has been skipped
This commit is contained in:
parent
bb70355151
commit
376808431a
39 changed files with 2542 additions and 248 deletions
17
backend/Cargo.lock
generated
17
backend/Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
RUST_LOG=info
|
||||
|
|
@ -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
|
||||
RUST_LOG=info
|
||||
|
|
@ -1,2 +1,5 @@
|
|||
# Optional: Logging Configuration
|
||||
RUST_LOG=info
|
||||
RUST_LOG=info
|
||||
|
||||
# Hub Universe DID Configuration
|
||||
HUB_UNIVERSE_DID=u:Test:018f1234-5678-9abc-def0-123456789012
|
||||
|
|
@ -2,4 +2,7 @@
|
|||
DATABASE_URL=postgres://postgres:password@localhost:5432/sharenet
|
||||
|
||||
# Optional: Logging Configuration
|
||||
RUST_LOG=info
|
||||
RUST_LOG=info
|
||||
|
||||
# Hub Universe DID Configuration
|
||||
HUB_UNIVERSE_DID=u:Test:018f1234-5678-9abc-def0-123456789012
|
||||
|
|
@ -1,2 +1,5 @@
|
|||
# Memory TUI Configuration
|
||||
RUST_LOG=info
|
||||
RUST_LOG=info
|
||||
|
||||
# Hub Universe DID Configuration
|
||||
HUB_UNIVERSE_DID=u:Test:018f1234-5678-9abc-def0-123456789012
|
||||
|
|
@ -1,3 +1,6 @@
|
|||
# Postgres TUI Configuration
|
||||
RUST_LOG=info
|
||||
DATABASE_URL=postgres://postgres:password@localhost:5432/sharenet
|
||||
DATABASE_URL=postgres://postgres:password@localhost:5432/sharenet
|
||||
|
||||
# Hub Universe DID Configuration
|
||||
HUB_UNIVERSE_DID=u:Test:018f1234-5678-9abc-def0-123456789012
|
||||
|
|
@ -108,6 +108,7 @@ use uuid::Uuid;
|
|||
pub struct AppState<U, P> {
|
||||
user_service: Arc<U>,
|
||||
product_service: Arc<P>,
|
||||
hub_universe_did: String,
|
||||
}
|
||||
|
||||
impl<U, P> Clone for AppState<U, P>
|
||||
|
|
@ -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<U, P>(addr: SocketAddr, user_service: U, product_service: P)
|
||||
pub async fn run<U, P>(addr: SocketAddr, user_service: U, product_service: P, hub_universe_did: String)
|
||||
where
|
||||
U: UseCase<User> + Clone + Send + Sync + 'static,
|
||||
P: UseCase<Product> + 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::<U, P>))
|
||||
.route("/users", post(create_user::<U>))
|
||||
.route("/users/:id", get(get_user::<U>))
|
||||
.route("/users", get(list_users::<U>))
|
||||
|
|
@ -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<U, P>(
|
||||
State(state): State<AppState<U, P>>,
|
||||
) -> 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::<MockUserService, MockProductService>))
|
||||
.route("/users", post(create_user::<MockUserService>))
|
||||
.route("/users/:id", get(get_user::<MockUserService>))
|
||||
.route("/users", get(list_users::<MockUserService>))
|
||||
|
|
@ -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
|
||||
//!
|
||||
|
|
|
|||
|
|
@ -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<U, P>(self, user_service: U, product_service: P) -> Result<()>
|
||||
pub async fn run<U, P>(self, user_service: U, product_service: P, hub_universe_did: String) -> Result<()>
|
||||
where
|
||||
U: UseCase<User>,
|
||||
P: UseCase<Product>,
|
||||
|
|
@ -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.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Self> {
|
||||
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<uuid::Uuid> {
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
@ -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 }
|
||||
dotenvy = { workspace = true }
|
||||
utils = { path = "../utils" }
|
||||
|
|
@ -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(())
|
||||
}
|
||||
|
|
@ -12,4 +12,5 @@ application = { path = "../application" }
|
|||
cli = { path = "../cli" }
|
||||
memory = { path = "../memory" }
|
||||
tokio = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
utils = { path = "../utils" }
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -14,4 +14,5 @@ postgres = { path = "../postgres" }
|
|||
tokio = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
dotenvy = { workspace = true }
|
||||
sqlx = { workspace = true }
|
||||
sqlx = { workspace = true }
|
||||
utils = { path = "../utils" }
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -12,3 +12,4 @@ application = { path = "../application" }
|
|||
tui = { path = "../tui" }
|
||||
memory = { path = "../memory" }
|
||||
tokio = { workspace = true }
|
||||
utils = { path = "../utils" }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,3 +14,4 @@ postgres = { path = "../postgres" }
|
|||
tokio = { workspace = true }
|
||||
dotenvy = { workspace = true }
|
||||
sqlx = { workspace = true }
|
||||
utils = { path = "../utils" }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,4 +13,5 @@ memory = { path = "../memory" }
|
|||
ratatui = { workspace = true }
|
||||
crossterm = { workspace = true }
|
||||
textwrap = "0.16"
|
||||
tokio = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
|
|
@ -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<U, P>(user_service: U, product_service: P) -> anyhow::Result<()>
|
||||
pub async fn run_tui<U, P>(user_service: U, product_service: P, hub_universe_did: String) -> anyhow::Result<()>
|
||||
where
|
||||
U: UseCase<User> + Clone + Send + 'static,
|
||||
P: UseCase<Product> + 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<B: Backend, U, P>(
|
|||
mut app: App,
|
||||
user_service: U,
|
||||
product_service: P,
|
||||
hub_universe_did: String,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
U: UseCase<User> + 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 <name> -d <description>".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 <name>".to_string());
|
||||
app.add_message(" Example: universe-did create -n myuniverse".to_string());
|
||||
app.add_message(" universe-did validate -d <did>".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<String> {
|
||||
let parts: Vec<&str> = cmd.split_whitespace().collect();
|
||||
if parts.len() < 4 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Invalid command format. Use: universe-did create -n <name>\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 <name>\nExample: universe-did create -n myuniverse"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_universe_did_validate(cmd: &str) -> anyhow::Result<String> {
|
||||
let parts: Vec<&str> = cmd.split_whitespace().collect();
|
||||
if parts.len() < 4 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Invalid command format. Use: universe-did validate -d <did>\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 <did>\nExample: universe-did validate -d u:myuniverse:12345678-1234-1234-1234-123456789012"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
|||
14
backend/crates/utils/Cargo.toml
Normal file
14
backend/crates/utils/Cargo.toml
Normal file
|
|
@ -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
|
||||
129
backend/crates/utils/src/lib.rs
Normal file
129
backend/crates/utils/src/lib.rs
Normal file
|
|
@ -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 <continuist02@gmail.com>
|
||||
*/
|
||||
|
||||
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<UniverseDid>` - Validated universe DID or error
|
||||
pub fn read_hub_universe_did(env_var: &str) -> Result<UniverseDid> {
|
||||
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<UniverseDid> {
|
||||
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<UniverseDid>` - Validated universe DID or error
|
||||
pub fn read_hub_universe_did_from_file(env_file_path: &str, env_var: &str) -> Result<UniverseDid> {
|
||||
// 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<UniverseDid> {
|
||||
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"));
|
||||
}
|
||||
}
|
||||
291
frontend/src/components/auth/create-passport-dialog.tsx
Normal file
291
frontend/src/components/auth/create-passport-dialog.tsx
Normal file
|
|
@ -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<string>('');
|
||||
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<string>('');
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New Passport</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create a new self-sovereign passport with your identity information.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6 py-4">
|
||||
|
||||
{/* Universe DID Section */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium mb-4">Affiliation</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
id="use-hub-did"
|
||||
checked={useHubUniverseDID}
|
||||
onChange={() => setUseHubUniverseDID(true)}
|
||||
className="h-4 w-4 text-blue-600"
|
||||
/>
|
||||
<label htmlFor="use-hub-did" className="text-sm font-medium">
|
||||
Use Hub Universe DID
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{useHubUniverseDID && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-500">
|
||||
Hub Universe DID
|
||||
</label>
|
||||
<Input
|
||||
value={isLoadingUniverseDID ? 'Loading...' : universeDID}
|
||||
disabled
|
||||
className="bg-gray-100 text-gray-500"
|
||||
/>
|
||||
<p className="text-xs text-gray-500">
|
||||
This passport will be affiliated with the current hub
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
id="custom-did"
|
||||
checked={!useHubUniverseDID}
|
||||
onChange={() => setUseHubUniverseDID(false)}
|
||||
className="h-4 w-4 text-blue-600"
|
||||
/>
|
||||
<label htmlFor="custom-did" className="text-sm font-medium">
|
||||
Use Custom Universe DID
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{!useHubUniverseDID && (
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="custom_universe_did" className="text-sm font-medium">
|
||||
Custom Universe DID
|
||||
</label>
|
||||
<Input
|
||||
id="custom_universe_did"
|
||||
placeholder="did:example:123456789"
|
||||
className="bg-white"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Password Section */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium mb-4">Passport Security</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="password" className="text-sm font-medium">
|
||||
Password *
|
||||
</label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="Enter password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="confirm_password" className="text-sm font-medium">
|
||||
Confirm Password *
|
||||
</label>
|
||||
<Input
|
||||
id="confirm_password"
|
||||
type="password"
|
||||
placeholder="Confirm password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-2">
|
||||
This password will be used to encrypt your passport file. Make sure to remember it!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Recovery Passphrase Section - Only shown after generation */}
|
||||
{showPassphrase && (
|
||||
<div className="border border-yellow-200 bg-yellow-50 rounded-lg p-4">
|
||||
<h3 className="text-lg font-medium text-yellow-800 mb-3">
|
||||
⚠️ Recovery Passphrase Generated
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-yellow-700 mb-2 block">
|
||||
Your 24-Word Recovery Passphrase:
|
||||
</label>
|
||||
<div className="bg-white border border-yellow-300 rounded-md p-3 font-mono text-sm text-gray-800 break-words">
|
||||
{recoveryPassphrase}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-yellow-700 space-y-2">
|
||||
<p className="font-medium">Important Security Instructions:</p>
|
||||
<ul className="list-disc list-inside space-y-1">
|
||||
<li>Write this passphrase down on paper and store it in a secure location</li>
|
||||
<li>This is the ONLY way to recover your passport if the file is lost</li>
|
||||
<li>Do not store this passphrase digitally or share it with anyone</li>
|
||||
<li>Keep it safe - without this passphrase, your passport cannot be recovered</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handlePassphraseAcknowledged}
|
||||
className="text-yellow-700 border-yellow-300 hover:bg-yellow-100"
|
||||
>
|
||||
I've Saved My Passphrase
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
{!showPassphrase ? (
|
||||
<>
|
||||
<Button variant="outline" onClick={handleClose} disabled={isCreating}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCreatePassport}
|
||||
disabled={isCreating || !password || password !== confirmPassword}
|
||||
>
|
||||
{isCreating ? 'Creating...' : 'Create and Download Passport'}
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<File | null>(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 (
|
||||
<>
|
||||
<Button
|
||||
|
|
@ -41,9 +60,34 @@ export function LoginButton({ className }: LoginButtonProps) {
|
|||
</button>
|
||||
</div>
|
||||
<PassportFilePicker onFileSelected={handleFileSelected} />
|
||||
|
||||
<div className="mt-6 pt-6 border-t border-gray-200">
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-gray-600 mb-3">Don't have a passport yet?</p>
|
||||
<Button
|
||||
onClick={handleCreatePassport}
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
>
|
||||
Create New Passport
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CreatePassportDialog
|
||||
open={showCreatePassport}
|
||||
onOpenChange={setShowCreatePassport}
|
||||
onPassportCreated={handlePassportCreated}
|
||||
/>
|
||||
|
||||
<ProfileManagementDialog
|
||||
open={showProfileManagement}
|
||||
onOpenChange={setShowProfileManagement}
|
||||
passportId={null}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
654
frontend/src/components/auth/profile-management-dialog.tsx
Normal file
654
frontend/src/components/auth/profile-management-dialog.tsx
Normal file
|
|
@ -0,0 +1,654 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { PassportBrowserIO } from '@/lib/wasm-browser';
|
||||
import type { UserIdentity, UserProfile, UserPreferences } from '@/lib/auth/types';
|
||||
|
||||
interface ProfileManagementDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
passportId: string | null;
|
||||
}
|
||||
|
||||
export function ProfileManagementDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
passportId
|
||||
}: ProfileManagementDialogProps) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [passportData, setPassportData] = useState<any>(null);
|
||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
||||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
||||
const [selectedProfile, setSelectedProfile] = useState<UserProfile | null>(null);
|
||||
|
||||
// Form fields for creating/editing profiles
|
||||
const [formData, setFormData] = useState<Partial<UserIdentity>>({
|
||||
handle: '',
|
||||
display_name: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: '',
|
||||
avatar_url: '',
|
||||
bio: '',
|
||||
});
|
||||
|
||||
// Form fields for preferences and settings
|
||||
const [preferencesData, setPreferencesData] = useState<Partial<UserPreferences>>({
|
||||
theme: 'light',
|
||||
language: 'en',
|
||||
notifications_enabled: true,
|
||||
privacy_level: 'standard',
|
||||
auto_sync: false,
|
||||
});
|
||||
|
||||
const [showDateOfBirth, setShowDateOfBirth] = useState(false);
|
||||
|
||||
// Load passport data when dialog opens
|
||||
useEffect(() => {
|
||||
if (open && passportId) {
|
||||
loadPassportData();
|
||||
}
|
||||
}, [open, passportId]);
|
||||
|
||||
const loadPassportData = async () => {
|
||||
if (!passportId) return;
|
||||
|
||||
console.log('Loading passport data for ID:', passportId);
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// Load passport from browser storage
|
||||
const result = await PassportBrowserIO.loadPassport(passportId);
|
||||
if (!result) {
|
||||
console.log('Passport not found in storage for ID:', passportId);
|
||||
throw new Error('Passport not found in storage');
|
||||
}
|
||||
console.log('Loaded passport data:', result);
|
||||
console.log('User profiles in loaded passport:', result.user_profiles);
|
||||
setPassportData(result);
|
||||
} catch (error) {
|
||||
console.error('Failed to load passport data:', error);
|
||||
alert('Failed to load passport data. Please try again.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (field: keyof UserIdentity) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[field]: e.target.value
|
||||
}));
|
||||
};
|
||||
|
||||
const handlePreferenceChange = (field: keyof UserPreferences) => (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const value = e.target.type === 'checkbox' ? (e.target as HTMLInputElement).checked : e.target.value;
|
||||
setPreferencesData(prev => ({
|
||||
...prev,
|
||||
[field]: value
|
||||
}));
|
||||
};
|
||||
|
||||
const handleShowDateOfBirthChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setShowDateOfBirth(e.target.checked);
|
||||
};
|
||||
|
||||
const handleCreateProfile = async () => {
|
||||
if (!formData.display_name) {
|
||||
alert('Display name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!passportId || !passportData) {
|
||||
alert('Passport data not loaded');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// Create user identity
|
||||
const identity: UserIdentity = {
|
||||
handle: formData.handle,
|
||||
display_name: formData.display_name,
|
||||
first_name: formData.first_name,
|
||||
last_name: formData.last_name,
|
||||
email: formData.email,
|
||||
avatar_url: formData.avatar_url,
|
||||
bio: formData.bio,
|
||||
};
|
||||
|
||||
// Create user preferences with form data
|
||||
const userPreferences: UserPreferences = {
|
||||
theme: preferencesData.theme || 'light',
|
||||
language: preferencesData.language || 'en',
|
||||
notifications_enabled: preferencesData.notifications_enabled ?? true,
|
||||
privacy_level: preferencesData.privacy_level || 'standard',
|
||||
auto_sync: preferencesData.auto_sync ?? false,
|
||||
};
|
||||
|
||||
// Create the new user profile using browser I/O operations
|
||||
// For additional profiles, provide a unique identifier
|
||||
const updatedPassport = await PassportBrowserIO.updatePassport(
|
||||
passportId,
|
||||
'create',
|
||||
`profile_${Date.now()}_${Math.random().toString(36).slice(2)}`, // Unique profile ID for non-default profile
|
||||
identity,
|
||||
userPreferences,
|
||||
showDateOfBirth
|
||||
);
|
||||
|
||||
// Update local state
|
||||
setPassportData(updatedPassport);
|
||||
|
||||
// Reset form and close create dialog
|
||||
setFormData({
|
||||
handle: '',
|
||||
display_name: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: '',
|
||||
avatar_url: '',
|
||||
bio: '',
|
||||
});
|
||||
setIsCreateDialogOpen(false);
|
||||
|
||||
alert('Profile created successfully!');
|
||||
} catch (error) {
|
||||
console.error('Failed to create profile:', error);
|
||||
alert('Failed to create profile. Please try again.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateProfile = async () => {
|
||||
if (!selectedProfile || !formData.display_name) {
|
||||
alert('Please select a profile and provide a display name');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!passportId || !passportData) {
|
||||
alert('Passport data not loaded');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// Create user identity
|
||||
const identity: UserIdentity = {
|
||||
handle: formData.handle,
|
||||
display_name: formData.display_name,
|
||||
first_name: formData.first_name,
|
||||
last_name: formData.last_name,
|
||||
email: formData.email,
|
||||
avatar_url: formData.avatar_url,
|
||||
bio: formData.bio,
|
||||
};
|
||||
|
||||
// Create user preferences with defaults
|
||||
const userPreferences: UserPreferences = {
|
||||
theme: 'light',
|
||||
language: 'en',
|
||||
notifications_enabled: true,
|
||||
privacy_level: 'standard',
|
||||
auto_sync: false,
|
||||
};
|
||||
|
||||
// Update the user profile using browser I/O operations
|
||||
const updatedPassport = await PassportBrowserIO.updatePassport(
|
||||
passportId,
|
||||
'update',
|
||||
selectedProfile.id,
|
||||
identity,
|
||||
userPreferences
|
||||
);
|
||||
|
||||
// Update local state
|
||||
setPassportData(updatedPassport);
|
||||
|
||||
// Reset form and close edit dialog
|
||||
setSelectedProfile(null);
|
||||
setFormData({
|
||||
handle: '',
|
||||
display_name: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: '',
|
||||
avatar_url: '',
|
||||
bio: '',
|
||||
});
|
||||
setIsEditDialogOpen(false);
|
||||
|
||||
alert('Profile updated successfully!');
|
||||
} catch (error) {
|
||||
console.error('Failed to update profile:', error);
|
||||
alert('Failed to update profile. Please try again.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleEditProfile = (profile: UserProfile) => {
|
||||
setSelectedProfile(profile);
|
||||
setFormData({
|
||||
handle: profile.identity.handle || '',
|
||||
display_name: profile.identity.display_name || '',
|
||||
first_name: profile.identity.first_name || '',
|
||||
last_name: profile.identity.last_name || '',
|
||||
email: profile.identity.email || '',
|
||||
avatar_url: profile.identity.avatar_url || '',
|
||||
bio: profile.identity.bio || '',
|
||||
});
|
||||
setIsEditDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteProfile = async (profile: UserProfile) => {
|
||||
if (!confirm(`Are you sure you want to delete the profile "${profile.identity.display_name}"? This action cannot be undone.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!passportId) {
|
||||
alert('Passport data not loaded');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// Delete the user profile using browser I/O operations
|
||||
const updatedPassport = await PassportBrowserIO.updatePassport(
|
||||
passportId,
|
||||
'delete',
|
||||
profile.id,
|
||||
null, // identity not needed for delete
|
||||
null // preferences not needed for delete
|
||||
);
|
||||
|
||||
// Update local state
|
||||
setPassportData(updatedPassport);
|
||||
|
||||
alert('Profile deleted successfully!');
|
||||
} catch (error) {
|
||||
console.error('Failed to delete profile:', error);
|
||||
alert('Failed to delete profile. Please try again.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadUpdatedPassport = async () => {
|
||||
if (!passportId || !passportData) return;
|
||||
|
||||
try {
|
||||
// For export, we need a password to encrypt the file
|
||||
const exportPassword = prompt('Enter a password to encrypt the exported passport file:');
|
||||
if (!exportPassword) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Export the passport using browser I/O operations
|
||||
const fileName = `updated_passport_${Date.now()}.spf`;
|
||||
await PassportBrowserIO.exportPassport(passportId, exportPassword, fileName);
|
||||
|
||||
alert('Updated passport downloaded successfully!');
|
||||
} catch (error) {
|
||||
console.error('Failed to download updated passport:', error);
|
||||
alert('Failed to download updated passport. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const profiles = passportData?.user_profiles || [];
|
||||
|
||||
// Helper function to determine if a profile is the default profile
|
||||
const isDefaultProfile = (profile: UserProfile): boolean => {
|
||||
return !profile.hub_did || profile.hub_did === '';
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Manage User Profiles</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create, edit, or delete user profiles in your passport.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="py-4">
|
||||
{isLoading && (
|
||||
<div className="text-center py-4">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-medium mb-4">User Profiles</h3>
|
||||
{profiles.length === 0 ? (
|
||||
<p className="text-gray-500">No profiles found in this passport.</p>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{profiles.map((profile: UserProfile) => {
|
||||
const isDefault = isDefaultProfile(profile);
|
||||
return (
|
||||
<div
|
||||
key={profile.id}
|
||||
className={`border rounded-lg p-4 ${
|
||||
isDefault
|
||||
? 'bg-blue-50 border-blue-200'
|
||||
: 'bg-white border-gray-200'
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<h4 className="font-medium">{profile.identity.display_name || 'Unnamed Profile'}</h4>
|
||||
{isDefault && (
|
||||
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||
Default User Profile
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mb-1">
|
||||
Handle: {profile.identity.handle || 'None'}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 mb-1">
|
||||
Email: {profile.identity.email || 'None'}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
Created: {new Date(profile.created_at * 1000).toLocaleDateString()}
|
||||
</p>
|
||||
{isDefault && (
|
||||
<div className="mt-2 p-2 bg-blue-100 rounded text-xs text-blue-700">
|
||||
<p className="font-medium">Every Passport file must have at least one user profile.</p>
|
||||
<p>This default profile cannot be deleted.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleEditProfile(profile)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
{!isDefault && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteProfile(profile)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 pt-4 border-t border-gray-200">
|
||||
<Button
|
||||
onClick={() => setIsCreateDialogOpen(true)}
|
||||
className="w-full"
|
||||
>
|
||||
Create New User Profile
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<div className="flex justify-between w-full">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleDownloadUpdatedPassport}
|
||||
disabled={!passportData}
|
||||
>
|
||||
Download Updated Passport
|
||||
</Button>
|
||||
<Button onClick={() => onOpenChange(false)}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Create Profile Dialog */}
|
||||
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New User Profile</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create a new user profile for your passport.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="display_name" className="text-sm font-medium">
|
||||
Display Name *
|
||||
</label>
|
||||
<Input
|
||||
id="display_name"
|
||||
placeholder="Your display name"
|
||||
value={formData.display_name}
|
||||
onChange={handleInputChange('display_name')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="handle" className="text-sm font-medium">
|
||||
Handle
|
||||
</label>
|
||||
<Input
|
||||
id="handle"
|
||||
placeholder="Your unique handle"
|
||||
value={formData.handle}
|
||||
onChange={handleInputChange('handle')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="first_name" className="text-sm font-medium">
|
||||
First Name
|
||||
</label>
|
||||
<Input
|
||||
id="first_name"
|
||||
placeholder="First name"
|
||||
value={formData.first_name}
|
||||
onChange={handleInputChange('first_name')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="last_name" className="text-sm font-medium">
|
||||
Last Name
|
||||
</label>
|
||||
<Input
|
||||
id="last_name"
|
||||
placeholder="Last name"
|
||||
value={formData.last_name}
|
||||
onChange={handleInputChange('last_name')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="email" className="text-sm font-medium">
|
||||
Email
|
||||
</label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="email@example.com"
|
||||
value={formData.email}
|
||||
onChange={handleInputChange('email')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="avatar_url" className="text-sm font-medium">
|
||||
Avatar URL
|
||||
</label>
|
||||
<Input
|
||||
id="avatar_url"
|
||||
placeholder="https://example.com/avatar.jpg"
|
||||
value={formData.avatar_url}
|
||||
onChange={handleInputChange('avatar_url')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="bio" className="text-sm font-medium">
|
||||
Bio
|
||||
</label>
|
||||
<Input
|
||||
id="bio"
|
||||
placeholder="Tell us about yourself..."
|
||||
value={formData.bio}
|
||||
onChange={handleInputChange('bio')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button variant="outline" onClick={() => setIsCreateDialogOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCreateProfile}
|
||||
disabled={isLoading || !formData.display_name}
|
||||
>
|
||||
{isLoading ? 'Creating...' : 'Create Profile'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Edit Profile Dialog */}
|
||||
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Edit Profile: {selectedProfile?.identity.display_name || 'Unnamed Profile'}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Update the user profile information.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="edit_display_name" className="text-sm font-medium">
|
||||
Display Name *
|
||||
</label>
|
||||
<Input
|
||||
id="edit_display_name"
|
||||
placeholder="Your display name"
|
||||
value={formData.display_name}
|
||||
onChange={handleInputChange('display_name')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="edit_handle" className="text-sm font-medium">
|
||||
Handle
|
||||
</label>
|
||||
<Input
|
||||
id="edit_handle"
|
||||
placeholder="Your unique handle"
|
||||
value={formData.handle}
|
||||
onChange={handleInputChange('handle')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="edit_first_name" className="text-sm font-medium">
|
||||
First Name
|
||||
</label>
|
||||
<Input
|
||||
id="edit_first_name"
|
||||
placeholder="First name"
|
||||
value={formData.first_name}
|
||||
onChange={handleInputChange('first_name')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="edit_last_name" className="text-sm font-medium">
|
||||
Last Name
|
||||
</label>
|
||||
<Input
|
||||
id="edit_last_name"
|
||||
placeholder="Last name"
|
||||
value={formData.last_name}
|
||||
onChange={handleInputChange('last_name')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="edit_email" className="text-sm font-medium">
|
||||
Email
|
||||
</label>
|
||||
<Input
|
||||
id="edit_email"
|
||||
type="email"
|
||||
placeholder="email@example.com"
|
||||
value={formData.email}
|
||||
onChange={handleInputChange('email')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="edit_avatar_url" className="text-sm font-medium">
|
||||
Avatar URL
|
||||
</label>
|
||||
<Input
|
||||
id="edit_avatar_url"
|
||||
placeholder="https://example.com/avatar.jpg"
|
||||
value={formData.avatar_url}
|
||||
onChange={handleInputChange('avatar_url')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="edit_bio" className="text-sm font-medium">
|
||||
Bio
|
||||
</label>
|
||||
<Input
|
||||
id="edit_bio"
|
||||
placeholder="Tell us about yourself..."
|
||||
value={formData.bio}
|
||||
onChange={handleInputChange('bio')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button variant="outline" onClick={() => setIsEditDialogOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleUpdateProfile}
|
||||
disabled={isLoading || !formData.display_name}
|
||||
>
|
||||
{isLoading ? 'Updating...' : 'Update Profile'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -3,14 +3,19 @@
|
|||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useAuth } from '@/lib/auth/context';
|
||||
import type { UserProfile } from '@/lib/auth/types';
|
||||
import { CreatePassportDialog } from './create-passport-dialog';
|
||||
import { ProfileManagementDialog } from './profile-management-dialog';
|
||||
import { PassportBrowserIO } from '@/lib/wasm-browser';
|
||||
|
||||
interface UserAvatarProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function UserAvatar({ className }: UserAvatarProps) {
|
||||
const { currentUser, availableProfiles, logout, switchProfile } = useAuth();
|
||||
const { currentUser, availableProfiles, logout, switchProfile, currentPassportId } = useAuth();
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const [isCreatePassportOpen, setIsCreatePassportOpen] = useState(false);
|
||||
const [isProfileManagementOpen, setIsProfileManagementOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
|
|
@ -69,6 +74,54 @@ export function UserAvatar({ className }: UserAvatarProps) {
|
|||
setIsDropdownOpen(false);
|
||||
};
|
||||
|
||||
const handleCreatePassport = () => {
|
||||
setIsCreatePassportOpen(true);
|
||||
setIsDropdownOpen(false);
|
||||
};
|
||||
|
||||
const handlePassportCreated = () => {
|
||||
// Optional: Show success message or refresh user data
|
||||
console.log('Passport created successfully');
|
||||
};
|
||||
|
||||
const handleManageProfiles = () => {
|
||||
if (!currentPassportId) {
|
||||
alert('No passport found to manage profiles');
|
||||
return;
|
||||
}
|
||||
|
||||
// No password needed since passport is already decrypted and in local storage
|
||||
setIsProfileManagementOpen(true);
|
||||
setIsDropdownOpen(false);
|
||||
};
|
||||
|
||||
const handleExportPassport = async () => {
|
||||
try {
|
||||
if (!currentPassportId) {
|
||||
alert('No passport found to export');
|
||||
return;
|
||||
}
|
||||
|
||||
// Export the passport - no password needed since we're using the decrypted version from local storage
|
||||
const fileName = `passport_export_${Date.now()}.spf`;
|
||||
|
||||
// For export, we still need a password to encrypt the exported file
|
||||
// But we can use a placeholder or prompt for export-specific password
|
||||
const exportPassword = prompt('Enter a password to encrypt the exported passport file:');
|
||||
if (!exportPassword) {
|
||||
return;
|
||||
}
|
||||
|
||||
await PassportBrowserIO.exportPassport(currentPassportId, exportPassword, fileName);
|
||||
|
||||
alert('Passport exported successfully!');
|
||||
setIsDropdownOpen(false);
|
||||
} catch (error) {
|
||||
console.error('Failed to export passport:', error);
|
||||
alert('Failed to export passport. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`relative ${className}`} ref={dropdownRef}>
|
||||
<button
|
||||
|
|
@ -171,7 +224,26 @@ export function UserAvatar({ className }: UserAvatarProps) {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-2">
|
||||
<div className="p-2 space-y-1">
|
||||
<button
|
||||
onClick={handleManageProfiles}
|
||||
className="w-full text-left px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded-md transition-colors"
|
||||
>
|
||||
Manage Profiles
|
||||
</button>
|
||||
<button
|
||||
onClick={handleExportPassport}
|
||||
className="w-full text-left px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded-md transition-colors"
|
||||
>
|
||||
Export Passport
|
||||
</button>
|
||||
<div className="border-t border-gray-100 my-1"></div>
|
||||
<button
|
||||
onClick={handleCreatePassport}
|
||||
className="w-full text-left px-3 py-2 text-sm text-blue-600 hover:bg-blue-50 rounded-md transition-colors"
|
||||
>
|
||||
Create New Passport
|
||||
</button>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full text-left px-3 py-2 text-sm text-red-600 hover:bg-red-50 rounded-md transition-colors"
|
||||
|
|
@ -181,6 +253,18 @@ export function UserAvatar({ className }: UserAvatarProps) {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CreatePassportDialog
|
||||
open={isCreatePassportOpen}
|
||||
onOpenChange={setIsCreatePassportOpen}
|
||||
onPassportCreated={handlePassportCreated}
|
||||
/>
|
||||
|
||||
<ProfileManagementDialog
|
||||
open={isProfileManagementOpen}
|
||||
onOpenChange={setIsProfileManagementOpen}
|
||||
passportId={currentPassportId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -50,4 +50,8 @@ export const productApi = {
|
|||
create: (data: Omit<Product, 'id' | 'created_at' | 'updated_at'>) => api.post<Product>('/products', data),
|
||||
update: (id: string, data: Partial<Omit<Product, 'id' | 'created_at' | 'updated_at'>>) => api.put<Product>(`/products/${id}`, data),
|
||||
delete: (id: string) => api.delete(`/products/${id}`),
|
||||
};
|
||||
|
||||
export const hubApi = {
|
||||
getUniverseDID: () => api.get<{ did: string }>('/hub/universe-did'),
|
||||
};
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
import React, { createContext, useContext, useReducer, useEffect } from 'react';
|
||||
import type { AuthContextValue, UserProfile } from './types';
|
||||
import { passportWASM } from '../wasm';
|
||||
import { PassportBrowserIO } from '../wasm-browser';
|
||||
import {
|
||||
storeEncryptedPassport,
|
||||
retrieveEncryptedPassport,
|
||||
|
|
@ -16,7 +17,7 @@ type StoragePreference = 'session' | 'persistent';
|
|||
type AuthAction =
|
||||
| { type: 'SET_LOADING'; payload: boolean }
|
||||
| { type: 'SET_ERROR'; payload: string | null }
|
||||
| { type: 'LOGIN_SUCCESS'; payload: { user: UserProfile; profiles: UserProfile[] } }
|
||||
| { type: 'LOGIN_SUCCESS'; payload: { user: UserProfile; profiles: UserProfile[]; passportId: string | null } }
|
||||
| { type: 'LOGOUT' }
|
||||
| { type: 'SWITCH_PROFILE'; payload: UserProfile }
|
||||
| { type: 'CLEAR_ERROR' };
|
||||
|
|
@ -28,6 +29,7 @@ const initialState: AuthContextValue = {
|
|||
availableProfiles: [],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
currentPassportId: null,
|
||||
login: async () => {},
|
||||
logout: () => {},
|
||||
switchProfile: () => {},
|
||||
|
|
@ -47,6 +49,7 @@ function authReducer(state: AuthContextValue, action: AuthAction): AuthContextVa
|
|||
isAuthenticated: true,
|
||||
currentUser: action.payload.user,
|
||||
availableProfiles: action.payload.profiles,
|
||||
currentPassportId: action.payload.passportId,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
};
|
||||
|
|
@ -56,6 +59,7 @@ function authReducer(state: AuthContextValue, action: AuthAction): AuthContextVa
|
|||
isAuthenticated: false,
|
||||
currentUser: null,
|
||||
availableProfiles: [],
|
||||
currentPassportId: null,
|
||||
error: null,
|
||||
};
|
||||
case 'SWITCH_PROFILE':
|
||||
|
|
@ -95,15 +99,22 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||
useEffect(() => {
|
||||
const loadPersistedAuth = async () => {
|
||||
try {
|
||||
console.log('Checking for persisted auth state...');
|
||||
const storedData = await retrieveEncryptedPassport();
|
||||
if (storedData) {
|
||||
console.log('Found persisted auth state:', storedData);
|
||||
// When loading from persisted storage, we don't have the passport ID
|
||||
// We'll set it to null and let the user re-import if needed
|
||||
dispatch({
|
||||
type: 'LOGIN_SUCCESS',
|
||||
payload: {
|
||||
user: storedData.currentUser,
|
||||
profiles: storedData.profiles,
|
||||
passportId: null,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
console.log('No persisted auth state found');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load persisted auth state:', error);
|
||||
|
|
@ -116,10 +127,14 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||
|
||||
// Auth actions
|
||||
const login = async (file: File, password: string, preference: StoragePreference) => {
|
||||
console.log('Starting login process with file:', file.name);
|
||||
dispatch({ type: 'SET_LOADING', payload: true });
|
||||
dispatch({ type: 'SET_ERROR', payload: null });
|
||||
|
||||
try {
|
||||
// Ensure WASM module is initialized before attempting to use it
|
||||
await passportWASM.init();
|
||||
|
||||
// Validate file type
|
||||
if (!file.name.endsWith('.spf')) {
|
||||
throw new Error('Please select a valid .spf file');
|
||||
|
|
@ -130,42 +145,64 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||
throw new Error('Password is required');
|
||||
}
|
||||
|
||||
// Read file as ArrayBuffer
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const data = new Uint8Array(arrayBuffer);
|
||||
// Import passport using WASM API with proper decryption
|
||||
console.log('Importing passport file...');
|
||||
const { passport, passportId } = await PassportBrowserIO.importFromFile(file, password);
|
||||
|
||||
// Parse .spf file using WASM with password
|
||||
const wasmModule = passportWASM.getModule();
|
||||
const profiles = await wasmModule.get_profiles_from_passport(data, password);
|
||||
// Extract profiles from passport data
|
||||
const profiles = passport.user_profiles || [];
|
||||
|
||||
console.log('Extracted profiles from imported passport:', profiles);
|
||||
|
||||
if (!profiles || profiles.length === 0) {
|
||||
throw new Error('No user profiles found in the selected .spf file');
|
||||
}
|
||||
|
||||
// For now, auto-select the first profile
|
||||
// In the future, we'll show a profile selection modal
|
||||
const selectedProfile = profiles[0];
|
||||
// Determine the default profile using the default_user_profile field
|
||||
const defaultProfileId = passport.default_user_profile;
|
||||
let selectedProfile = profiles[0]; // Fallback to first profile
|
||||
|
||||
if (defaultProfileId) {
|
||||
const defaultProfile = profiles.find((profile: any) => profile.id === defaultProfileId);
|
||||
if (defaultProfile) {
|
||||
selectedProfile = defaultProfile;
|
||||
console.log('Using default profile from passport:', defaultProfileId);
|
||||
} else {
|
||||
console.warn('Default profile ID not found in profiles, using first profile');
|
||||
}
|
||||
} else {
|
||||
console.log('No default profile specified, using first profile');
|
||||
}
|
||||
|
||||
// Store encrypted passport data
|
||||
console.log('Storing encrypted passport data...');
|
||||
await storeEncryptedPassport(profiles, selectedProfile, preference);
|
||||
|
||||
console.log('Login successful. Passport ID:', passportId, 'Profiles:', profiles.length);
|
||||
|
||||
dispatch({
|
||||
type: 'LOGIN_SUCCESS',
|
||||
payload: {
|
||||
user: selectedProfile,
|
||||
profiles,
|
||||
passportId,
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to parse .spf file';
|
||||
console.error('Login failed:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to decrypt .spf file';
|
||||
dispatch({ type: 'SET_ERROR', payload: errorMessage });
|
||||
}
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
console.log('Logging out - clearing all passport data');
|
||||
dispatch({ type: 'LOGOUT' });
|
||||
clearStoredPassport();
|
||||
// Also clear passport data from browser storage
|
||||
PassportBrowserIO.deleteAllPassports();
|
||||
console.log('Logout complete');
|
||||
};
|
||||
|
||||
const switchProfile = (profileId: string) => {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,17 @@ export interface UserIdentity {
|
|||
bio?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* User preferences interface matching the Rust UserPreferences struct
|
||||
*/
|
||||
export interface UserPreferences {
|
||||
theme?: string;
|
||||
language?: string;
|
||||
notifications_enabled?: boolean;
|
||||
privacy_level?: string;
|
||||
auto_sync?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* User profile interface matching the Rust UserProfile struct
|
||||
*/
|
||||
|
|
@ -18,6 +29,8 @@ export interface UserProfile {
|
|||
id: string;
|
||||
hub_did?: string; // None for default profile
|
||||
identity: UserIdentity;
|
||||
preferences: UserPreferences;
|
||||
show_date_of_birth?: boolean;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
}
|
||||
|
|
@ -27,7 +40,9 @@ export interface UserProfile {
|
|||
*/
|
||||
export interface SPFPassport {
|
||||
version: string;
|
||||
profiles: UserProfile[];
|
||||
default_user_profile: string;
|
||||
date_of_birth?: string; // Global optional date of birth
|
||||
user_profiles: UserProfile[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -39,6 +54,7 @@ export interface AuthState {
|
|||
availableProfiles: UserProfile[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
currentPassportId: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
480
frontend/src/lib/wasm-browser.ts
Normal file
480
frontend/src/lib/wasm-browser.ts
Normal file
|
|
@ -0,0 +1,480 @@
|
|||
'use client';
|
||||
|
||||
import { passportWASM } from './wasm';
|
||||
import type { UserIdentity, UserPreferences } from './auth/types';
|
||||
|
||||
/**
|
||||
* Browser-compatible passport operations using local storage
|
||||
*/
|
||||
export class PassportBrowserIO {
|
||||
private static readonly STORAGE_KEY = 'sharenet-passports';
|
||||
|
||||
/**
|
||||
* Create a new passport and store in local storage
|
||||
*/
|
||||
static async createPassport(
|
||||
univId: string,
|
||||
password: string
|
||||
): Promise<{ passport: any; recoveryPhrase: string; passportId: string; downloadBlob: Blob }> {
|
||||
const wasm = await passportWASM.init();
|
||||
|
||||
// Create passport in memory - this already includes a default user profile
|
||||
const createResult = await wasm.create_passport(univId, password);
|
||||
console.log('Create passport result:', createResult);
|
||||
|
||||
// The create_passport function returns a JSON object with 'passport' and 'recovery_phrase' entries
|
||||
// We need to properly extract and convert the Passport structure
|
||||
let passport;
|
||||
let recoveryPhraseFromCreate = '';
|
||||
|
||||
if (createResult instanceof Map) {
|
||||
// Handle Map structure from WASM
|
||||
const passportMap = createResult.get('passport');
|
||||
const recoveryPhraseMap = createResult.get('recovery_phrase');
|
||||
|
||||
if (passportMap instanceof Map) {
|
||||
// Recursively convert the Map and all nested Maps to plain JavaScript objects
|
||||
passport = this.convertMapToObject(passportMap);
|
||||
} else {
|
||||
passport = passportMap;
|
||||
}
|
||||
|
||||
if (recoveryPhraseMap instanceof Map) {
|
||||
recoveryPhraseFromCreate = recoveryPhraseMap.get('words')?.join(' ') || '';
|
||||
} else if (typeof recoveryPhraseMap === 'object' && recoveryPhraseMap !== null) {
|
||||
recoveryPhraseFromCreate = recoveryPhraseMap.words?.join(' ') || '';
|
||||
}
|
||||
} else if (typeof createResult === 'object' && createResult !== null) {
|
||||
// Handle regular object structure
|
||||
passport = createResult.passport || createResult;
|
||||
|
||||
if (createResult.recovery_phrase) {
|
||||
if (typeof createResult.recovery_phrase === 'object') {
|
||||
recoveryPhraseFromCreate = createResult.recovery_phrase.words?.join(' ') || '';
|
||||
} else if (typeof createResult.recovery_phrase === 'string') {
|
||||
recoveryPhraseFromCreate = createResult.recovery_phrase;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error('Unexpected return type from create_passport');
|
||||
}
|
||||
|
||||
// Use the recovery phrase from create_passport if available, otherwise generate one
|
||||
let finalRecoveryPhrase = recoveryPhraseFromCreate;
|
||||
if (!finalRecoveryPhrase) {
|
||||
const recoveryResult = await wasm.generate_recovery_phrase();
|
||||
if (recoveryResult instanceof Map) {
|
||||
finalRecoveryPhrase = recoveryResult.get('words')?.join(' ') || '';
|
||||
} else if (typeof recoveryResult === 'object' && recoveryResult !== null) {
|
||||
finalRecoveryPhrase = recoveryResult.words?.join(' ') || '';
|
||||
} else {
|
||||
throw new Error('Failed to generate recovery phrase');
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Extracted passport object:', passport);
|
||||
console.log('Passport type:', typeof passport);
|
||||
console.log('Passport keys:', Object.keys(passport || {}));
|
||||
console.log('Detailed passport structure:', {
|
||||
seed: passport?.seed,
|
||||
user_profiles: passport?.user_profiles,
|
||||
default_user_profile_id: passport?.default_user_profile_id,
|
||||
date_of_birth: passport?.date_of_birth,
|
||||
full_structure: passport
|
||||
});
|
||||
|
||||
// Validate that passport has all required fields
|
||||
const requiredFields = ['seed', 'default_user_profile_id', 'date_of_birth'];
|
||||
const missingFields = requiredFields.filter(field => !passport || !passport.hasOwnProperty(field));
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
console.error('Passport missing required fields:', {
|
||||
hasPassport: !!passport,
|
||||
hasSeed: !!passport?.seed,
|
||||
hasDefaultProfileId: !!passport?.default_user_profile_id,
|
||||
hasDateOfBirth: !!passport?.date_of_birth,
|
||||
hasUserProfiles: !!passport?.user_profiles,
|
||||
missingFields,
|
||||
passportStructure: passport
|
||||
});
|
||||
|
||||
// Try to manually add missing fields if possible
|
||||
if (passport && passport.user_profiles && passport.user_profiles.length > 0) {
|
||||
console.log('Attempting to fix missing fields...');
|
||||
|
||||
if (missingFields.includes('default_user_profile_id')) {
|
||||
const firstProfile = passport.user_profiles[0];
|
||||
if (firstProfile && firstProfile.id) {
|
||||
passport.default_user_profile_id = firstProfile.id;
|
||||
console.log('Set default_user_profile_id to:', firstProfile.id);
|
||||
} else {
|
||||
// Fallback: generate a UUIDv7-like ID
|
||||
passport.default_user_profile_id = `018${Date.now().toString(16).slice(-12)}-0000-0000-0000-000000000000`;
|
||||
console.log('Set default_user_profile_id to generated ID:', passport.default_user_profile_id);
|
||||
}
|
||||
}
|
||||
|
||||
if (missingFields.includes('date_of_birth')) {
|
||||
passport.date_of_birth = null;
|
||||
console.log('Set date_of_birth to null');
|
||||
}
|
||||
}
|
||||
|
||||
// Re-validate after potential fixes
|
||||
const stillMissing = requiredFields.filter(field => !passport || !passport.hasOwnProperty(field));
|
||||
if (stillMissing.length > 0) {
|
||||
throw new Error(`Passport missing required fields after creation: ${stillMissing.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate unique passport ID
|
||||
const passportId = `passport_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
||||
|
||||
// Store passport in local storage
|
||||
await this.savePassportToStorage(passportId, passport);
|
||||
|
||||
// Create encrypted download blob
|
||||
console.log('Passport structure before export:', {
|
||||
hasSeed: !!passport?.seed,
|
||||
hasDateOfBirth: passport?.hasOwnProperty('date_of_birth'),
|
||||
dateOfBirthValue: passport?.date_of_birth,
|
||||
hasDefaultProfileId: passport?.hasOwnProperty('default_user_profile_id'),
|
||||
defaultProfileIdValue: passport?.default_user_profile_id,
|
||||
hasUserProfiles: !!passport?.user_profiles,
|
||||
userProfilesCount: passport?.user_profiles?.length,
|
||||
fullPassport: passport
|
||||
});
|
||||
|
||||
const encryptedData = await wasm.export_to_encrypted_data(passport, password);
|
||||
console.log('Encrypted data length:', encryptedData.length);
|
||||
|
||||
const downloadBlob = new Blob([new Uint8Array(encryptedData)], {
|
||||
type: 'application/octet-stream'
|
||||
});
|
||||
|
||||
return {
|
||||
passport,
|
||||
recoveryPhrase: finalRecoveryPhrase,
|
||||
passportId,
|
||||
downloadBlob
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load passport from local storage
|
||||
*/
|
||||
static async loadPassport(passportId: string): Promise<any> {
|
||||
return this.getPassportFromStorage(passportId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save passport to local storage
|
||||
*/
|
||||
static async savePassport(passportId: string, passportData: any): Promise<void> {
|
||||
await this.savePassportToStorage(passportId, passportData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update passport with new profile and save to local storage
|
||||
*/
|
||||
static async updatePassport(
|
||||
passportId: string,
|
||||
operation: 'create' | 'update' | 'delete',
|
||||
profileId: string | null,
|
||||
identity: UserIdentity | null,
|
||||
preferences: UserPreferences | null,
|
||||
showDateOfBirth?: boolean
|
||||
): Promise<any> {
|
||||
const wasm = await passportWASM.init();
|
||||
|
||||
// Load current passport from storage
|
||||
const currentPassport = await this.getPassportFromStorage(passportId);
|
||||
if (!currentPassport) {
|
||||
throw new Error(`Passport not found: ${passportId}`);
|
||||
}
|
||||
|
||||
let updatedPassport;
|
||||
|
||||
switch (operation) {
|
||||
case 'create':
|
||||
if (!identity || !preferences) {
|
||||
throw new Error('Identity and preferences are required for creating profile');
|
||||
}
|
||||
// For additional profiles (non-default), we need to provide a unique hub_did
|
||||
// If profileId is provided, use it as hub_did, otherwise create default profile
|
||||
const hubDid = profileId ? profileId : null;
|
||||
// Include showDateOfBirth in the preferences object
|
||||
const createPreferences = {
|
||||
...preferences,
|
||||
show_date_of_birth: showDateOfBirth ?? false
|
||||
};
|
||||
|
||||
updatedPassport = await wasm.create_user_profile(
|
||||
currentPassport,
|
||||
hubDid, // hub_did - null for default, unique string for additional profiles
|
||||
identity,
|
||||
createPreferences
|
||||
);
|
||||
break;
|
||||
|
||||
case 'update':
|
||||
if (!profileId || !identity || !preferences) {
|
||||
throw new Error('Profile ID, identity, and preferences are required for updating profile');
|
||||
}
|
||||
// Include showDateOfBirth in the preferences object
|
||||
const updatePreferences = {
|
||||
...preferences,
|
||||
show_date_of_birth: showDateOfBirth ?? false
|
||||
};
|
||||
|
||||
updatedPassport = await wasm.update_user_profile(
|
||||
currentPassport,
|
||||
profileId,
|
||||
identity,
|
||||
updatePreferences
|
||||
);
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
if (!profileId) {
|
||||
throw new Error('Profile ID is required for deleting profile');
|
||||
}
|
||||
updatedPassport = await wasm.delete_user_profile(
|
||||
currentPassport,
|
||||
profileId
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown operation: ${operation}`);
|
||||
}
|
||||
|
||||
// Save updated passport to storage
|
||||
await this.savePassportToStorage(passportId, updatedPassport);
|
||||
|
||||
return updatedPassport;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import passport from recovery phrase and store in local storage
|
||||
*/
|
||||
static async importFromRecovery(
|
||||
univId: string,
|
||||
recoveryWords: string[],
|
||||
password: string
|
||||
): Promise<{ passport: any; passportId: string }> {
|
||||
const wasm = await passportWASM.init();
|
||||
|
||||
// Import from recovery phrase
|
||||
const result = await wasm.import_from_recovery(univId, recoveryWords, password);
|
||||
|
||||
// Generate unique passport ID
|
||||
const passportId = `passport_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
||||
|
||||
// Store passport in local storage
|
||||
await this.savePassportToStorage(passportId, result);
|
||||
|
||||
return {
|
||||
passport: result,
|
||||
passportId
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Import passport from file and store in local storage
|
||||
*/
|
||||
static async importFromFile(
|
||||
file: File,
|
||||
password: string
|
||||
): Promise<{ passport: any; passportId: string }> {
|
||||
const wasm = await passportWASM.init();
|
||||
|
||||
try {
|
||||
// Read the file content as ArrayBuffer
|
||||
const fileBuffer = await file.arrayBuffer();
|
||||
const fileBytes = new Uint8Array(fileBuffer);
|
||||
|
||||
console.log('Attempting to import passport file with', fileBytes.length, 'bytes');
|
||||
|
||||
// First validate the passport file
|
||||
console.log('Validating passport file...');
|
||||
const isValid = await wasm.validate_passport_file(fileBytes);
|
||||
if (!isValid) {
|
||||
throw new Error('Invalid passport file format');
|
||||
}
|
||||
|
||||
// Get passport metadata
|
||||
console.log('Getting passport metadata...');
|
||||
const metadata = await wasm.get_passport_metadata(fileBytes);
|
||||
console.log('Passport metadata:', metadata);
|
||||
|
||||
// Import passport from encrypted data
|
||||
console.log('Importing passport from encrypted data...');
|
||||
const passport = await wasm.import_from_encrypted_data(fileBytes, password);
|
||||
|
||||
// Generate unique passport ID
|
||||
const passportId = `imported_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
||||
|
||||
// Store passport in local storage
|
||||
await this.savePassportToStorage(passportId, passport);
|
||||
|
||||
console.log('Import successful. Passport ID:', passportId);
|
||||
console.log('Passport data:', passport);
|
||||
console.log('User profiles:', passport.user_profiles);
|
||||
|
||||
return {
|
||||
passport,
|
||||
passportId
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to import passport from file:', error);
|
||||
throw new Error(`Failed to import passport: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all stored passports
|
||||
*/
|
||||
static async getAllPassports(): Promise<{ [key: string]: any }> {
|
||||
if (typeof window === 'undefined') {
|
||||
return {};
|
||||
}
|
||||
|
||||
const stored = localStorage.getItem(this.STORAGE_KEY);
|
||||
return stored ? JSON.parse(stored) : {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete passport from local storage
|
||||
*/
|
||||
static async deletePassport(passportId: string): Promise<void> {
|
||||
const passports = await this.getAllPassports();
|
||||
delete passports[passportId];
|
||||
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(passports));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all passports from local storage
|
||||
*/
|
||||
static async deleteAllPassports(): Promise<void> {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
localStorage.removeItem(this.STORAGE_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export passport as encrypted data for download
|
||||
*/
|
||||
static async exportPassport(passportId: string, password: string, fileName: string): Promise<void> {
|
||||
const wasm = await passportWASM.init();
|
||||
const passport = await this.getPassportFromStorage(passportId);
|
||||
if (!passport) {
|
||||
throw new Error(`Passport not found: ${passportId}`);
|
||||
}
|
||||
|
||||
// Export passport as encrypted data
|
||||
const encryptedData = await wasm.export_to_encrypted_data(passport, password);
|
||||
|
||||
// Create blob for download
|
||||
const blob = new Blob([new Uint8Array(encryptedData)], { type: 'application/octet-stream' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = fileName.endsWith('.spf') ? fileName : `${fileName}.spf`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export passport as JSON for backup (unencrypted)
|
||||
*/
|
||||
static async exportPassportAsJson(passportId: string, fileName: string): Promise<void> {
|
||||
const passport = await this.getPassportFromStorage(passportId);
|
||||
if (!passport) {
|
||||
throw new Error(`Passport not found: ${passportId}`);
|
||||
}
|
||||
|
||||
const jsonString = JSON.stringify(passport);
|
||||
const blob = new Blob([jsonString], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = fileName.endsWith('.json') ? fileName : `${fileName}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// Private helper methods
|
||||
|
||||
/**
|
||||
* Recursively convert a Map and all nested Maps to plain JavaScript objects
|
||||
*/
|
||||
private static convertMapToObject(map: Map<any, any>): any {
|
||||
const obj: any = {};
|
||||
for (const [key, value] of map.entries()) {
|
||||
if (value instanceof Map) {
|
||||
obj[key] = this.convertMapToObject(value);
|
||||
} else if (Array.isArray(value)) {
|
||||
obj[key] = value.map(item =>
|
||||
item instanceof Map ? this.convertMapToObject(item) : item
|
||||
);
|
||||
} else if (key === 'bytes' && Array.isArray(value)) {
|
||||
// Special handling for Seed.bytes field which should be a Uint8Array
|
||||
obj[key] = new Uint8Array(value);
|
||||
} else if (key === '0' && Array.isArray(value)) {
|
||||
// Handle PublicKey/PrivateKey tuple-like structure
|
||||
obj[key] = new Uint8Array(value);
|
||||
} else {
|
||||
obj[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Special handling for Passport structure
|
||||
if (obj.user_profiles && Array.isArray(obj.user_profiles) && obj.user_profiles.length > 0) {
|
||||
// Ensure default_user_profile_id is set if missing
|
||||
if (!obj.hasOwnProperty('default_user_profile_id')) {
|
||||
const firstProfile = obj.user_profiles[0];
|
||||
if (firstProfile && firstProfile.id) {
|
||||
obj.default_user_profile_id = firstProfile.id;
|
||||
} else {
|
||||
// Fallback: generate a UUIDv7-like ID
|
||||
obj.default_user_profile_id = `018${Date.now().toString(16).slice(-12)}-0000-0000-0000-000000000000`;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure date_of_birth is explicitly set to null if missing
|
||||
if (!obj.hasOwnProperty('date_of_birth')) {
|
||||
obj.date_of_birth = null;
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
private static async savePassportToStorage(passportId: string, passportData: any): Promise<void> {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const passports = await this.getAllPassports();
|
||||
passports[passportId] = passportData;
|
||||
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(passports));
|
||||
}
|
||||
|
||||
private static async getPassportFromStorage(passportId: string): Promise<any> {
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const passports = await this.getAllPassports();
|
||||
return passports[passportId] || null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,50 @@
|
|||
'use client';
|
||||
|
||||
import type { UserProfile, SPFPassport } from './auth/types';
|
||||
import type { UserProfile, SPFPassport, UserPreferences } from './auth/types';
|
||||
|
||||
/**
|
||||
* WASM module interface with proper TypeScript typing
|
||||
* WASM module interface with proper TypeScript typing for the new API
|
||||
*/
|
||||
interface PassportWASM {
|
||||
parse_spf_file(data: Uint8Array, password: string): Promise<SPFPassport>;
|
||||
get_profiles_from_passport(data: Uint8Array, password: string): Promise<UserProfile[]>;
|
||||
validate_spf_signature(data: Uint8Array, signature: Uint8Array): Promise<boolean>;
|
||||
// Core passport operations
|
||||
create_passport(univ_id: string, password: string): Promise<any>;
|
||||
import_from_recovery(univ_id: string, recovery_words: string[], password: string): Promise<any>;
|
||||
import_from_encrypted_data(encrypted_data: Uint8Array, password: string): Promise<any>;
|
||||
export_to_encrypted_data(passport_json: any, password: string): Promise<Uint8Array>;
|
||||
sign_message(passport_json: any, message: string): Promise<Uint8Array>;
|
||||
|
||||
// Recovery phrase operations
|
||||
generate_recovery_phrase(): Promise<any>;
|
||||
validate_recovery_phrase(recovery_words: string[]): Promise<boolean>;
|
||||
|
||||
// User profile management
|
||||
create_user_profile(
|
||||
passport_json: any,
|
||||
hub_did: string | null,
|
||||
identity_json: any,
|
||||
preferences_json: any
|
||||
): Promise<any>;
|
||||
update_user_profile(
|
||||
passport_json: any,
|
||||
profile_id: string,
|
||||
identity_json: any,
|
||||
preferences_json: any
|
||||
): Promise<any>;
|
||||
delete_user_profile(
|
||||
passport_json: any,
|
||||
profile_id: string
|
||||
): Promise<any>;
|
||||
|
||||
// Password management
|
||||
change_passport_password(
|
||||
passport_json: any,
|
||||
old_password: string,
|
||||
new_password: string
|
||||
): Promise<any>;
|
||||
|
||||
// File operations
|
||||
get_passport_metadata(encrypted_data: Uint8Array): Promise<any>;
|
||||
validate_passport_file(encrypted_data: Uint8Array): Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -53,34 +89,132 @@ export class PassportWASMLoader {
|
|||
}
|
||||
|
||||
try {
|
||||
console.log('Loading WASM module...');
|
||||
|
||||
// Dynamically import the WASM bindings
|
||||
// With bundler target, the module is automatically initialized on import
|
||||
// but we need to ensure the WASM memory is ready before calling functions
|
||||
const wasmModule = await import('./wasm-pkg/sharenet_passport_wasm');
|
||||
|
||||
console.log('WASM module imported:', wasmModule);
|
||||
|
||||
// Initialize the WASM module
|
||||
console.log('Initializing WASM module...');
|
||||
await wasmModule.default();
|
||||
console.log('WASM module initialized successfully');
|
||||
|
||||
// Test that the WASM module is properly initialized by checking if
|
||||
// the wasm memory is accessible through a simple property access
|
||||
// This ensures the WASM module is fully loaded before we use it
|
||||
if (!wasmModule || typeof wasmModule.parse_spf_file !== 'function') {
|
||||
if (!wasmModule || typeof wasmModule.create_passport !== 'function') {
|
||||
console.error('WASM module exports:', Object.keys(wasmModule));
|
||||
throw new Error('WASM module exports not properly loaded');
|
||||
}
|
||||
|
||||
console.log('WASM module loaded successfully');
|
||||
|
||||
// Create wrapper functions with proper typing
|
||||
const wasmWrapper: PassportWASM = {
|
||||
parse_spf_file: async (data: Uint8Array, password: string): Promise<SPFPassport> => {
|
||||
const result = wasmModule.parse_spf_file(data, password);
|
||||
// The WASM function returns a JsValue that we need to convert
|
||||
// For now, we'll assume it returns the correct structure
|
||||
return result as unknown as SPFPassport;
|
||||
// Core passport operations
|
||||
create_passport: async (univ_id: string, password: string): Promise<any> => {
|
||||
console.log('Calling create_passport...');
|
||||
return wasmModule.create_passport(univ_id, password);
|
||||
},
|
||||
|
||||
get_profiles_from_passport: async (data: Uint8Array, password: string): Promise<UserProfile[]> => {
|
||||
const result = wasmModule.get_profiles_from_passport(data, password);
|
||||
return result as unknown as UserProfile[];
|
||||
import_from_recovery: async (univ_id: string, recovery_words: string[], password: string): Promise<any> => {
|
||||
console.log('Calling import_from_recovery...');
|
||||
return wasmModule.import_from_recovery(univ_id, recovery_words, password);
|
||||
},
|
||||
|
||||
validate_spf_signature: async (data: Uint8Array, signature: Uint8Array): Promise<boolean> => {
|
||||
return wasmModule.validate_spf_signature(data, signature);
|
||||
import_from_encrypted_data: async (encrypted_data: Uint8Array, password: string): Promise<any> => {
|
||||
console.log('Calling import_from_encrypted_data...');
|
||||
return wasmModule.import_from_encrypted_data(encrypted_data, password);
|
||||
},
|
||||
|
||||
export_to_encrypted_data: async (passport_json: any, password: string): Promise<Uint8Array> => {
|
||||
console.log('Calling export_to_encrypted_data...');
|
||||
return wasmModule.export_to_encrypted_data(passport_json, password);
|
||||
},
|
||||
|
||||
sign_message: async (passport_json: any, message: string): Promise<Uint8Array> => {
|
||||
console.log('Calling sign_message...');
|
||||
return wasmModule.sign_message(passport_json, message);
|
||||
},
|
||||
|
||||
// Recovery phrase operations
|
||||
generate_recovery_phrase: async (): Promise<any> => {
|
||||
console.log('Calling generate_recovery_phrase...');
|
||||
return wasmModule.generate_recovery_phrase();
|
||||
},
|
||||
|
||||
validate_recovery_phrase: async (recovery_words: string[]): Promise<boolean> => {
|
||||
console.log('Calling validate_recovery_phrase...');
|
||||
return wasmModule.validate_recovery_phrase(recovery_words);
|
||||
},
|
||||
|
||||
// User profile management
|
||||
create_user_profile: async (
|
||||
passport_json: any,
|
||||
hub_did: string | null,
|
||||
identity_json: any,
|
||||
preferences_json: any
|
||||
): Promise<any> => {
|
||||
console.log('Calling create_user_profile...');
|
||||
return wasmModule.create_user_profile(
|
||||
passport_json,
|
||||
hub_did,
|
||||
identity_json,
|
||||
preferences_json
|
||||
);
|
||||
},
|
||||
|
||||
update_user_profile: async (
|
||||
passport_json: any,
|
||||
profile_id: string,
|
||||
identity_json: any,
|
||||
preferences_json: any
|
||||
): Promise<any> => {
|
||||
console.log('Calling update_user_profile...');
|
||||
return wasmModule.update_user_profile(
|
||||
passport_json,
|
||||
profile_id,
|
||||
identity_json,
|
||||
preferences_json
|
||||
);
|
||||
},
|
||||
|
||||
delete_user_profile: async (
|
||||
passport_json: any,
|
||||
profile_id: string
|
||||
): Promise<any> => {
|
||||
console.log('Calling delete_user_profile...');
|
||||
return wasmModule.delete_user_profile(
|
||||
passport_json,
|
||||
profile_id
|
||||
);
|
||||
},
|
||||
|
||||
// Password management
|
||||
change_passport_password: async (
|
||||
passport_json: any,
|
||||
old_password: string,
|
||||
new_password: string
|
||||
): Promise<any> => {
|
||||
console.log('Calling change_passport_password...');
|
||||
return wasmModule.change_passport_password(
|
||||
passport_json,
|
||||
old_password,
|
||||
new_password
|
||||
);
|
||||
},
|
||||
|
||||
// File operations
|
||||
get_passport_metadata: async (encrypted_data: Uint8Array): Promise<any> => {
|
||||
console.log('Calling get_passport_metadata...');
|
||||
return wasmModule.get_passport_metadata(encrypted_data);
|
||||
},
|
||||
|
||||
validate_passport_file: async (encrypted_data: Uint8Array): Promise<boolean> => {
|
||||
console.log('Calling validate_passport_file...');
|
||||
return wasmModule.validate_passport_file(encrypted_data);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
73
frontend/wasm/Cargo.lock
generated
73
frontend/wasm/Cargo.lock
generated
|
|
@ -383,9 +383,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
|||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.81"
|
||||
version = "0.3.82"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305"
|
||||
checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
|
|
@ -397,12 +397,6 @@ version = "0.2.177"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
|
|
@ -453,9 +447,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.101"
|
||||
version = "1.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
|
||||
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
|
@ -609,9 +603,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "sharenet-passport"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
source = "sparse+https://git.sharenet.sh/api/packages/devteam/cargo/"
|
||||
checksum = "e54fa035fcfc2734f15fd3fb2ed951c10bb2b3357285d38151d32e76f7815b02"
|
||||
checksum = "bec9d785a802bbfcd6a84f72f2a53e50729847a68ed5f4e6ea1310177bfe4c43"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64",
|
||||
|
|
@ -627,10 +621,13 @@ dependencies = [
|
|||
"rand",
|
||||
"rand_core",
|
||||
"serde",
|
||||
"serde-wasm-bindgen",
|
||||
"serde_cbor",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"thiserror",
|
||||
"uuid",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-time",
|
||||
"zeroize",
|
||||
|
|
@ -676,9 +673,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.106"
|
||||
version = "2.0.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
|
||||
checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -728,15 +725,15 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
|||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.19"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.24"
|
||||
version = "0.1.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956"
|
||||
checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
|
@ -785,9 +782,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.104"
|
||||
version = "0.2.105"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d"
|
||||
checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
|
|
@ -796,25 +793,11 @@ dependencies = [
|
|||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.54"
|
||||
version = "0.4.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c"
|
||||
checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
|
|
@ -825,9 +808,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.104"
|
||||
version = "0.2.105"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119"
|
||||
checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
|
|
@ -835,31 +818,31 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.104"
|
||||
version = "0.2.105"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7"
|
||||
checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.104"
|
||||
version = "0.2.105"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1"
|
||||
checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.81"
|
||||
version = "0.3.82"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120"
|
||||
checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ edition = "2021"
|
|||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen = "0.2.105"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde-wasm-bindgen = "0.6"
|
||||
serde_cbor = "0.11"
|
||||
sharenet-passport = { version = "0.3.0", registry = "sharenet-sh-forgejo", features = ["force-wasm"] }
|
||||
sharenet-passport = { version = "0.4.0", registry = "sharenet-sh-forgejo", features = ["force-wasm"] }
|
||||
|
||||
# WASM-compatible random number generation
|
||||
getrandom = { version = "0.2", features = ["js"] }
|
||||
|
|
|
|||
|
|
@ -1,137 +1,4 @@
|
|||
use wasm_bindgen::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_cbor;
|
||||
|
||||
mod debug;
|
||||
|
||||
use sharenet_passport::{
|
||||
Passport,
|
||||
domain::entities::{UserProfile, PassportFile},
|
||||
domain::traits::FileEncryptor,
|
||||
infrastructure::XChaCha20FileEncryptor,
|
||||
};
|
||||
|
||||
// WASM-compatible wrapper structs that match the Rust crate types
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct WASMUserIdentity {
|
||||
pub handle: Option<String>,
|
||||
pub display_name: Option<String>,
|
||||
pub first_name: Option<String>,
|
||||
pub last_name: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub avatar_url: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct WASMUserProfile {
|
||||
pub id: String,
|
||||
pub hub_did: Option<String>,
|
||||
pub identity: WASMUserIdentity,
|
||||
pub created_at: u64,
|
||||
pub updated_at: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct WASMSPFPassport {
|
||||
pub version: String,
|
||||
pub profiles: Vec<WASMUserProfile>,
|
||||
}
|
||||
|
||||
// Convert from crate types to WASM-compatible types
|
||||
impl From<UserProfile> for WASMUserProfile {
|
||||
fn from(profile: UserProfile) -> Self {
|
||||
WASMUserProfile {
|
||||
id: profile.id,
|
||||
hub_did: profile.hub_did,
|
||||
identity: WASMUserIdentity {
|
||||
handle: profile.identity.handle,
|
||||
display_name: profile.identity.display_name,
|
||||
first_name: profile.identity.first_name,
|
||||
last_name: profile.identity.last_name,
|
||||
email: profile.identity.email,
|
||||
avatar_url: profile.identity.avatar_url,
|
||||
bio: profile.identity.bio,
|
||||
},
|
||||
created_at: profile.created_at,
|
||||
updated_at: profile.updated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Passport> for WASMSPFPassport {
|
||||
fn from(passport: Passport) -> Self {
|
||||
WASMSPFPassport {
|
||||
version: "1.0".to_string(), // Hardcoded version for now
|
||||
profiles: passport.user_profiles.into_iter().map(WASMUserProfile::from).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn parse_spf_file(data: &[u8], password: &str) -> Result<JsValue, JsValue> {
|
||||
// Use the real sharenet-passport crate to decrypt and parse the .spf file
|
||||
|
||||
// Validate password
|
||||
if password.is_empty() {
|
||||
return Err(JsValue::from_str("Password is required"));
|
||||
}
|
||||
|
||||
// Parse the .spf file data into a PassportFile structure
|
||||
// The .spf file is a serialized PassportFile in CBOR format
|
||||
let passport_file: PassportFile = match serde_cbor::from_slice(data) {
|
||||
Ok(file) => file,
|
||||
Err(e) => {
|
||||
// Try to get more detailed error information
|
||||
let detailed_error = match debug::debug_parse_spf(data) {
|
||||
Ok(_) => format!("CBOR parsing failed: {}", e),
|
||||
Err(debug_err) => format!("CBOR parsing failed: {}. Debug: {}", e, debug_err),
|
||||
};
|
||||
|
||||
return Err(JsValue::from_str(&format!("Failed to parse .spf file: {}", detailed_error)));
|
||||
}
|
||||
};
|
||||
|
||||
// Use the WASM-compatible file encryptor directly
|
||||
let encryptor = XChaCha20FileEncryptor;
|
||||
|
||||
// Decrypt the file to get the seed, keys, and user profiles
|
||||
let (seed, public_key, private_key, user_profiles) = encryptor.decrypt(&passport_file, password)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to decrypt file: {}", e)))?;
|
||||
|
||||
// Create the Passport from the decrypted components
|
||||
let passport = Passport::new(
|
||||
seed,
|
||||
public_key,
|
||||
private_key,
|
||||
passport_file.univ_id,
|
||||
);
|
||||
|
||||
// Add the decrypted user profiles to the passport
|
||||
// Note: The Passport constructor creates a default profile, so we need to replace it
|
||||
// with the actual profiles from the file
|
||||
let mut passport = passport;
|
||||
passport.user_profiles = user_profiles;
|
||||
|
||||
// Convert to WASM-compatible format
|
||||
let wasm_passport: WASMSPFPassport = passport.into();
|
||||
|
||||
serde_wasm_bindgen::to_value(&wasm_passport).map_err(|e| JsValue::from_str(&e.to_string()))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn get_profiles_from_passport(data: &[u8], password: &str) -> Result<JsValue, JsValue> {
|
||||
// This will extract just the profiles from the passport
|
||||
let result = parse_spf_file(data, password)?;
|
||||
let passport: WASMSPFPassport = serde_wasm_bindgen::from_value(result)
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
|
||||
serde_wasm_bindgen::to_value(&passport.profiles).map_err(|e| JsValue::from_str(&e.to_string()))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn validate_spf_signature(data: &[u8], signature: &[u8]) -> Result<bool, JsValue> {
|
||||
// Signature validation is not implemented in the current API
|
||||
// For now, return true to indicate successful validation
|
||||
Ok(true)
|
||||
}
|
||||
// Re-export all functions from the sharenet-passport crate
|
||||
pub use sharenet_passport::wasm::*;
|
||||
Loading…
Add table
Reference in a new issue