feature/1-self-sovereign-passports-for-user-identity #2
59 changed files with 5057 additions and 83 deletions
|
|
@ -140,7 +140,7 @@ jobs:
|
||||||
|
|
||||||
build-backend:
|
build-backend:
|
||||||
runs-on: [ci]
|
runs-on: [ci]
|
||||||
# if: false
|
if: false
|
||||||
# needs: [test-backend, test-frontend]
|
# needs: [test-backend, test-frontend]
|
||||||
# needs: [test-frontend]
|
# needs: [test-frontend]
|
||||||
|
|
||||||
|
|
@ -176,7 +176,7 @@ jobs:
|
||||||
|
|
||||||
build-frontend:
|
build-frontend:
|
||||||
runs-on: [ci]
|
runs-on: [ci]
|
||||||
if: false
|
# if: false
|
||||||
# needs: [test-backend, test-frontend]
|
# needs: [test-backend, test-frontend]
|
||||||
# needs: [test-frontend]
|
# needs: [test-frontend]
|
||||||
|
|
||||||
|
|
@ -208,6 +208,7 @@ jobs:
|
||||||
echo "=== Inode info ==="
|
echo "=== Inode info ==="
|
||||||
df -i /home/ci-service /tmp /var/tmp 2>/dev/null || df -i /tmp /var/tmp
|
df -i /home/ci-service /tmp /var/tmp 2>/dev/null || df -i /tmp /var/tmp
|
||||||
|
|
||||||
|
|
||||||
- name: Build frontend container image
|
- name: Build frontend container image
|
||||||
run: |
|
run: |
|
||||||
# Create temp directory on larger filesystem
|
# Create temp directory on larger filesystem
|
||||||
|
|
@ -236,8 +237,8 @@ jobs:
|
||||||
|
|
||||||
deploy-prod:
|
deploy-prod:
|
||||||
runs-on: [prod]
|
runs-on: [prod]
|
||||||
needs: [build-backend]
|
# needs: [build-backend]
|
||||||
# needs: [build-frontend]
|
needs: [build-frontend]
|
||||||
# needs: [build-backend, build-frontend]
|
# needs: [build-backend, build-frontend]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
|
@ -292,19 +293,19 @@ jobs:
|
||||||
docker.io/nginx:alpine \
|
docker.io/nginx:alpine \
|
||||||
sh -lc 'nginx -t -c /etc/nginx/nginx.conf'
|
sh -lc 'nginx -t -c /etc/nginx/nginx.conf'
|
||||||
|
|
||||||
# APPLY/RE-APPLY THE POD (no explicit "down"; use --replace)
|
# If --replace is NOT supported in your Podman, use this fallback instead:
|
||||||
- name: Apply pod (kube play --replace)
|
- name: Recreate pod (fallback)
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
# If your Podman supports --replace, this is the cleanest:
|
podman --remote pod rm -f sharenet-production-pod 2>/dev/null || true
|
||||||
envsubst < deploy/prod-pod.yml | podman --remote kube play --replace -
|
envsubst < deploy/prod-pod.yml | podman --remote kube play -
|
||||||
|
|
||||||
# If --replace is NOT supported in your Podman, use this fallback instead:
|
# If --replace IS supported in your Podman, use this instead:
|
||||||
# - name: Recreate pod (fallback)
|
# - name: Apply pod (kube play --replace)
|
||||||
# run: |
|
# run: |
|
||||||
# set -euo pipefail
|
# set -euo pipefail
|
||||||
# podman --remote pod rm -f sharenet-production-pod 2>/dev/null || true
|
# # If your Podman supports --replace, this is the cleanest:
|
||||||
# envsubst < deploy/prod-pod.yml | podman --remote kube play -
|
# envsubst < deploy/prod-pod.yml | podman --remote kube play --replace -
|
||||||
|
|
||||||
# VERIFY (install curl first)
|
# VERIFY (install curl first)
|
||||||
- name: Verify in-pod Nginx
|
- name: Verify in-pod Nginx
|
||||||
|
|
|
||||||
17
backend/Cargo.lock
generated
17
backend/Cargo.lock
generated
|
|
@ -1893,6 +1893,7 @@ dependencies = [
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"memory",
|
"memory",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1908,6 +1909,7 @@ dependencies = [
|
||||||
"postgres",
|
"postgres",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1921,6 +1923,7 @@ dependencies = [
|
||||||
"domain",
|
"domain",
|
||||||
"memory",
|
"memory",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1936,6 +1939,7 @@ dependencies = [
|
||||||
"postgres",
|
"postgres",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1948,6 +1952,7 @@ dependencies = [
|
||||||
"memory",
|
"memory",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tui",
|
"tui",
|
||||||
|
"utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1962,6 +1967,7 @@ dependencies = [
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tui",
|
"tui",
|
||||||
|
"utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2633,6 +2639,7 @@ dependencies = [
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"textwrap",
|
"textwrap",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2733,6 +2740,16 @@ version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utils"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"domain",
|
||||||
|
"dotenvy",
|
||||||
|
"tempfile",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.17.0"
|
version = "1.17.0"
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ tokio = { version = "1.36", features = ["full"] }
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
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"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
clap = { version = "4.5", features = ["derive"] }
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
|
|
@ -29,6 +29,7 @@ dotenvy = "0.15"
|
||||||
ratatui = "0.24"
|
ratatui = "0.24"
|
||||||
crossterm = "0.27"
|
crossterm = "0.27"
|
||||||
textwrap = "0.16"
|
textwrap = "0.16"
|
||||||
|
tempfile = "3.10"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
|
|
|
||||||
|
|
@ -2,5 +2,8 @@
|
||||||
HOST=127.0.0.1
|
HOST=127.0.0.1
|
||||||
PORT=3001
|
PORT=3001
|
||||||
|
|
||||||
|
# Hub Universe DID Configuration
|
||||||
|
HUB_UNIVERSE_DID=u:Test:018f1234-5678-9abc-def0-123456789012
|
||||||
|
|
||||||
# Optional: Logging Configuration
|
# Optional: Logging Configuration
|
||||||
RUST_LOG=info
|
RUST_LOG=info
|
||||||
|
|
@ -5,5 +5,8 @@ PORT=3001
|
||||||
# Database Configuration
|
# Database Configuration
|
||||||
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
|
||||||
|
|
||||||
# Optional: Logging Configuration
|
# Optional: Logging Configuration
|
||||||
RUST_LOG=info
|
RUST_LOG=info
|
||||||
|
|
@ -1,2 +1,5 @@
|
||||||
# Optional: Logging Configuration
|
# Optional: Logging Configuration
|
||||||
RUST_LOG=info
|
RUST_LOG=info
|
||||||
|
|
||||||
|
# Hub Universe DID Configuration
|
||||||
|
HUB_UNIVERSE_DID=u:Test:018f1234-5678-9abc-def0-123456789012
|
||||||
|
|
@ -3,3 +3,6 @@ DATABASE_URL=postgres://postgres:password@localhost:5432/sharenet
|
||||||
|
|
||||||
# Optional: Logging Configuration
|
# 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
|
# 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
|
# Postgres TUI Configuration
|
||||||
RUST_LOG=info
|
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> {
|
pub struct AppState<U, P> {
|
||||||
user_service: Arc<U>,
|
user_service: Arc<U>,
|
||||||
product_service: Arc<P>,
|
product_service: Arc<P>,
|
||||||
|
hub_universe_did: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<U, P> Clone for AppState<U, P>
|
impl<U, P> Clone for AppState<U, P>
|
||||||
|
|
@ -119,6 +120,7 @@ where
|
||||||
Self {
|
Self {
|
||||||
user_service: self.user_service.clone(),
|
user_service: self.user_service.clone(),
|
||||||
product_service: self.product_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
|
/// * `addr` - The socket address to bind the server to
|
||||||
/// * `user_service` - Service implementation for user operations
|
/// * `user_service` - Service implementation for user operations
|
||||||
/// * `product_service` - Service implementation for product 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.
|
/// 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
|
where
|
||||||
U: UseCase<User> + Clone + Send + Sync + 'static,
|
U: UseCase<User> + Clone + Send + Sync + 'static,
|
||||||
P: UseCase<Product> + Clone + Send + Sync + 'static,
|
P: UseCase<Product> + Clone + Send + Sync + 'static,
|
||||||
|
|
@ -150,6 +153,7 @@ where
|
||||||
let state = AppState {
|
let state = AppState {
|
||||||
user_service: Arc::new(user_service),
|
user_service: Arc::new(user_service),
|
||||||
product_service: Arc::new(product_service),
|
product_service: Arc::new(product_service),
|
||||||
|
hub_universe_did,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Configure CORS
|
// Configure CORS
|
||||||
|
|
@ -160,6 +164,7 @@ where
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/health", get(health_check))
|
.route("/health", get(health_check))
|
||||||
|
.route("/hub/universe-did", get(get_universe_did::<U, P>))
|
||||||
.route("/users", post(create_user::<U>))
|
.route("/users", post(create_user::<U>))
|
||||||
.route("/users/:id", get(get_user::<U>))
|
.route("/users/:id", get(get_user::<U>))
|
||||||
.route("/users", get(list_users::<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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
//! # API Tests
|
//! # API Tests
|
||||||
|
|
@ -586,10 +607,12 @@ mod tests {
|
||||||
let state = AppState {
|
let state = AppState {
|
||||||
user_service: Arc::new(user_service),
|
user_service: Arc::new(user_service),
|
||||||
product_service: Arc::new(product_service),
|
product_service: Arc::new(product_service),
|
||||||
|
hub_universe_did: "u:hub:12345678-1234-1234-1234-123456789012".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/health", get(health_check))
|
.route("/health", get(health_check))
|
||||||
|
.route("/hub/universe-did", get(get_universe_did::<MockUserService, MockProductService>))
|
||||||
.route("/users", post(create_user::<MockUserService>))
|
.route("/users", post(create_user::<MockUserService>))
|
||||||
.route("/users/:id", get(get_user::<MockUserService>))
|
.route("/users/:id", get(get_user::<MockUserService>))
|
||||||
.route("/users", get(list_users::<MockUserService>))
|
.route("/users", get(list_users::<MockUserService>))
|
||||||
|
|
@ -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 {
|
mod product_endpoints {
|
||||||
//! # Product Endpoint Tests
|
//! # Product Endpoint Tests
|
||||||
//!
|
//!
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use domain::{CreateProduct, CreateUser, Product, User, UpdateProduct, UpdateUser};
|
use domain::{CreateProduct, CreateUser, Product, User, UpdateProduct, UpdateUser, UniverseDid};
|
||||||
use application::UseCase;
|
use application::UseCase;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
|
@ -34,6 +34,11 @@ pub enum Commands {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: ProductCommands,
|
command: ProductCommands,
|
||||||
},
|
},
|
||||||
|
/// Universe DID management commands
|
||||||
|
UniverseDid {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: UniverseDidCommands,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[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 {
|
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
|
where
|
||||||
U: UseCase<User>,
|
U: UseCase<User>,
|
||||||
P: UseCase<Product>,
|
P: UseCase<Product>,
|
||||||
|
|
@ -167,6 +192,54 @@ impl Cli {
|
||||||
println!("Deleted product {}", id);
|
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 => {
|
None => {
|
||||||
println!("No command provided. Use --help for usage information.");
|
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"] }
|
tokio = { workspace = true, features = ["full", "macros", "rt-multi-thread"] }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
dotenvy = { workspace = true }
|
dotenvy = { workspace = true }
|
||||||
|
utils = { path = "../utils" }
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,12 @@ use application::Service;
|
||||||
use domain::{User, Product};
|
use domain::{User, Product};
|
||||||
use memory::{InMemoryProductRepository, InMemoryUserRepository};
|
use memory::{InMemoryProductRepository, InMemoryUserRepository};
|
||||||
use std::env;
|
use std::env;
|
||||||
|
use utils::HubConfig;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
// Load environment variables from config file
|
// Load and validate hub universe DID from environment file
|
||||||
dotenvy::from_path("config/api-memory.env").ok();
|
let hub_universe_did = HubConfig::read_default_hub_universe_did_from_file("config/api-memory.env")?;
|
||||||
|
|
||||||
// Get configuration from environment variables
|
// Get configuration from environment variables
|
||||||
let host = env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
|
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
|
// Run API server
|
||||||
let addr = format!("{}:{}", host, port);
|
let addr = format!("{}:{}", host, port);
|
||||||
let addr = SocketAddr::from_str(&addr)?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -15,3 +15,4 @@ tokio = { workspace = true, features = ["full", "macros", "rt-multi-thread"] }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
sqlx = { 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 domain::{User, Product};
|
||||||
use postgres::{PostgresProductRepository, PostgresUserRepository};
|
use postgres::{PostgresProductRepository, PostgresUserRepository};
|
||||||
use sqlx::postgres::PgPoolOptions;
|
use sqlx::postgres::PgPoolOptions;
|
||||||
use dotenvy;
|
|
||||||
use std::env;
|
use std::env;
|
||||||
|
use utils::HubConfig;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
// Load environment variables from config file
|
// Load and validate hub universe DID from environment file
|
||||||
dotenvy::from_path("config/api-postgres.env").ok();
|
let hub_universe_did = HubConfig::read_default_hub_universe_did_from_file("config/api-postgres.env")?;
|
||||||
|
|
||||||
// Get configuration from environment variables
|
// Get configuration from environment variables
|
||||||
let host = env::var("HOST").unwrap_or_else(|_| "0.0.0.0".to_string());
|
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
|
// Run API server
|
||||||
let addr = format!("{}:{}", host, port);
|
let addr = format!("{}:{}", host, port);
|
||||||
let addr = SocketAddr::from_str(&addr)?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -13,3 +13,4 @@ cli = { path = "../cli" }
|
||||||
memory = { path = "../memory" }
|
memory = { path = "../memory" }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
clap = { workspace = true }
|
clap = { workspace = true }
|
||||||
|
utils = { path = "../utils" }
|
||||||
|
|
@ -2,12 +2,16 @@ use anyhow::Result;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use memory::{MemoryUserService, MemoryProductService};
|
use memory::{MemoryUserService, MemoryProductService};
|
||||||
use cli::Cli;
|
use cli::Cli;
|
||||||
|
use utils::HubConfig;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
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 user_service = MemoryUserService::new(memory::InMemoryUserRepository::new());
|
||||||
let product_service = MemoryProductService::new(memory::InMemoryProductRepository::new());
|
let product_service = MemoryProductService::new(memory::InMemoryProductRepository::new());
|
||||||
|
|
||||||
let cli = Cli::try_parse()?;
|
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
|
||||||
}
|
}
|
||||||
|
|
@ -15,3 +15,4 @@ tokio = { workspace = true }
|
||||||
clap = { workspace = true }
|
clap = { workspace = true }
|
||||||
dotenvy = { 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 clap::Parser;
|
||||||
use postgres::{PostgresUserService, PostgresProductService};
|
use postgres::{PostgresUserService, PostgresProductService};
|
||||||
use cli::Cli;
|
use cli::Cli;
|
||||||
|
use utils::HubConfig;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
dotenvy::from_path("config/cli-postgres.env").ok();
|
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 pool = sqlx::PgPool::connect(&std::env::var("DATABASE_URL")?).await?;
|
||||||
let user_service = PostgresUserService::new(postgres::PostgresUserRepository::new(pool.clone()));
|
let user_service = PostgresUserService::new(postgres::PostgresUserRepository::new(pool.clone()));
|
||||||
let product_service = PostgresProductService::new(postgres::PostgresProductRepository::new(pool));
|
let product_service = PostgresProductService::new(postgres::PostgresProductRepository::new(pool));
|
||||||
|
|
||||||
let cli = Cli::try_parse()?;
|
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" }
|
tui = { path = "../tui" }
|
||||||
memory = { path = "../memory" }
|
memory = { path = "../memory" }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
|
utils = { path = "../utils" }
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use memory::{MemoryUserService, MemoryProductService};
|
use memory::{MemoryUserService, MemoryProductService};
|
||||||
use tui::run_tui;
|
use tui::run_tui;
|
||||||
|
use utils::HubConfig;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
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 user_service = MemoryUserService::new(memory::InMemoryUserRepository::new());
|
||||||
let product_service = MemoryProductService::new(memory::InMemoryProductRepository::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 }
|
tokio = { workspace = true }
|
||||||
dotenvy = { workspace = true }
|
dotenvy = { workspace = true }
|
||||||
sqlx = { workspace = true }
|
sqlx = { workspace = true }
|
||||||
|
utils = { path = "../utils" }
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use postgres::{PostgresUserService, PostgresProductService};
|
use postgres::{PostgresUserService, PostgresProductService};
|
||||||
use tui::run_tui;
|
use tui::run_tui;
|
||||||
|
use utils::HubConfig;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
|
|
@ -36,8 +37,11 @@ async fn main() -> Result<()> {
|
||||||
Err(e) => println!("DATABASE_URL not found: {}", e),
|
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 pool = sqlx::PgPool::connect(&std::env::var("DATABASE_URL")?).await?;
|
||||||
let user_service = PostgresUserService::new(postgres::PostgresUserRepository::new(pool.clone()));
|
let user_service = PostgresUserService::new(postgres::PostgresUserRepository::new(pool.clone()));
|
||||||
let product_service = PostgresProductService::new(postgres::PostgresProductRepository::new(pool));
|
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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,3 +14,4 @@ ratatui = { workspace = true }
|
||||||
crossterm = { workspace = true }
|
crossterm = { workspace = true }
|
||||||
textwrap = "0.16"
|
textwrap = "0.16"
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
|
uuid = { workspace = true }
|
||||||
|
|
@ -21,7 +21,7 @@ use crossterm::{
|
||||||
execute,
|
execute,
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
};
|
};
|
||||||
use domain::{CreateProduct, CreateUser, Product, User};
|
use domain::{CreateProduct, CreateUser, Product, User, UniverseDid};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::{Backend, CrosstermBackend},
|
backend::{Backend, CrosstermBackend},
|
||||||
layout::{Constraint, Direction, Layout},
|
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
|
where
|
||||||
U: UseCase<User> + Clone + Send + 'static,
|
U: UseCase<User> + Clone + Send + 'static,
|
||||||
P: UseCase<Product> + Clone + Send + 'static,
|
P: UseCase<Product> + Clone + Send + 'static,
|
||||||
|
|
@ -156,7 +156,7 @@ where
|
||||||
|
|
||||||
// Create app and run it
|
// Create app and run it
|
||||||
let app = App::new();
|
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
|
// Restore terminal
|
||||||
disable_raw_mode()?;
|
disable_raw_mode()?;
|
||||||
|
|
@ -179,6 +179,7 @@ async fn run_app<B: Backend, U, P>(
|
||||||
mut app: App,
|
mut app: App,
|
||||||
user_service: U,
|
user_service: U,
|
||||||
product_service: P,
|
product_service: P,
|
||||||
|
hub_universe_did: String,
|
||||||
) -> anyhow::Result<()>
|
) -> anyhow::Result<()>
|
||||||
where
|
where
|
||||||
U: UseCase<User> + Clone + Send + 'static,
|
U: UseCase<User> + Clone + Send + 'static,
|
||||||
|
|
@ -307,6 +308,62 @@ where
|
||||||
Err(e) => app.add_message(format!("Error: {}", e)),
|
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());
|
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(" 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(" Example: product create -n \"My Product\" -d \"A great product description\"".to_string());
|
||||||
app.add_message(" product list".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("\nTips:".to_string());
|
||||||
app.add_message(" - Use quotes for values with spaces".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());
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
4
frontend/.eslintrc.json
Normal file
4
frontend/.eslintrc.json
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"extends": "next/core-web-vitals",
|
||||||
|
"ignorePatterns": ["src/lib/wasm-pkg/**"]
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,30 @@
|
||||||
|
# ---------- wasm-builder ----------
|
||||||
|
# Cache bust: Force WASM rebuild with cargo registry fix
|
||||||
|
FROM docker.io/rust:1.90-slim AS wasm-builder
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Set CARGO_HOME to ensure cargo uses our configuration
|
||||||
|
ENV CARGO_HOME=/root/.cargo
|
||||||
|
# Cache busting environment variable to force WASM rebuild
|
||||||
|
ENV CACHE_BUST=20241025
|
||||||
|
|
||||||
|
# Install wasm32 target and wasm-pack
|
||||||
|
RUN rustup target add wasm32-unknown-unknown
|
||||||
|
RUN cargo install wasm-pack --root /usr/local
|
||||||
|
|
||||||
|
# Configure cargo registry for sharenet-sh-forgejo
|
||||||
|
RUN mkdir -p $CARGO_HOME
|
||||||
|
RUN echo '[registries.sharenet-sh-forgejo]' > $CARGO_HOME/config.toml
|
||||||
|
RUN echo 'index = "sparse+https://git.sharenet.sh/api/packages/devteam/cargo/"' >> $CARGO_HOME/config.toml
|
||||||
|
RUN echo '' >> $CARGO_HOME/config.toml
|
||||||
|
RUN echo '[net]' >> $CARGO_HOME/config.toml
|
||||||
|
RUN echo 'git-fetch-with-cli = true' >> $CARGO_HOME/config.toml
|
||||||
|
|
||||||
|
# Copy WASM source and build
|
||||||
|
COPY wasm/Cargo.toml wasm/Cargo.lock ./wasm/
|
||||||
|
COPY wasm/src ./wasm/src/
|
||||||
|
RUN cd wasm && wasm-pack build --target bundler
|
||||||
|
|
||||||
# ---------- build ----------
|
# ---------- build ----------
|
||||||
FROM docker.io/node:20-slim AS builder
|
FROM docker.io/node:20-slim AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
@ -6,7 +33,8 @@ WORKDIR /app
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm ci --no-audit --no-fund --prefer-offline
|
RUN npm ci --no-audit --no-fund --prefer-offline
|
||||||
|
|
||||||
# Copy app source
|
# Copy app source and WASM artifacts
|
||||||
|
COPY --from=wasm-builder /app/wasm/pkg ./src/lib/wasm-pkg/
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# disable telemetry; let Next control NODE_ENV during build
|
# disable telemetry; let Next control NODE_ENV during build
|
||||||
|
|
@ -40,6 +68,9 @@ COPY --from=builder /app/public ./public
|
||||||
COPY --from=builder /app/.next/standalone ./
|
COPY --from=builder /app/.next/standalone ./
|
||||||
COPY --from=builder /app/.next/static ./.next/static
|
COPY --from=builder /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
# Copy WASM files to standalone output
|
||||||
|
COPY --from=builder /app/src/lib/wasm-pkg ./src/lib/wasm-pkg/
|
||||||
|
|
||||||
# non-root (optional)
|
# non-root (optional)
|
||||||
RUN addgroup --system --gid 1001 nodejs \
|
RUN addgroup --system --gid 1001 nodejs \
|
||||||
&& adduser --system --uid 1001 nextjs \
|
&& adduser --system --uid 1001 nextjs \
|
||||||
|
|
|
||||||
46
frontend/debug_spf.html
Normal file
46
frontend/debug_spf.html
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>SPF File Debug</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>SPF File Debug</h1>
|
||||||
|
<input type="file" id="spfFile" accept=".spf">
|
||||||
|
<div id="output"></div>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import init, { parse_spf_file } from './wasm/pkg/sharenet_passport_wasm.js';
|
||||||
|
|
||||||
|
await init();
|
||||||
|
|
||||||
|
document.getElementById('spfFile').addEventListener('change', async (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const output = document.getElementById('output');
|
||||||
|
output.innerHTML = `<p>Loading file: ${file.name} (${file.size} bytes)</p>`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
|
const data = new Uint8Array(arrayBuffer);
|
||||||
|
|
||||||
|
output.innerHTML += `<p>File loaded: ${data.length} bytes</p>`;
|
||||||
|
output.innerHTML += `<p>First 16 bytes: ${Array.from(data.slice(0, 16)).map(b => b.toString(16).padStart(2, '0')).join(' ')}</p>`;
|
||||||
|
|
||||||
|
// Try to parse with password "test"
|
||||||
|
const password = prompt('Enter password:');
|
||||||
|
if (!password) return;
|
||||||
|
|
||||||
|
output.innerHTML += `<p>Attempting to parse with password...</p>`;
|
||||||
|
|
||||||
|
const result = await parse_spf_file(data, password);
|
||||||
|
output.innerHTML += `<p style="color: green;">Success! Parsed SPF file</p>`;
|
||||||
|
output.innerHTML += `<pre>${JSON.stringify(result, null, 2)}</pre>`;
|
||||||
|
} catch (error) {
|
||||||
|
output.innerHTML += `<p style="color: red;">Error: ${error.message}</p>`;
|
||||||
|
console.error('SPF parsing error:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -22,6 +22,32 @@ const nextConfig: NextConfig = {
|
||||||
|
|
||||||
// Webpack optimizations
|
// Webpack optimizations
|
||||||
webpack: (config, { dev, isServer }) => {
|
webpack: (config, { dev, isServer }) => {
|
||||||
|
// Enable WASM support
|
||||||
|
config.experiments = {
|
||||||
|
...config.experiments,
|
||||||
|
asyncWebAssembly: true,
|
||||||
|
syncWebAssembly: false,
|
||||||
|
layers: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Configure WASM file handling
|
||||||
|
config.module = {
|
||||||
|
...config.module,
|
||||||
|
rules: [
|
||||||
|
...(config.module?.rules || []),
|
||||||
|
{
|
||||||
|
test: /\.wasm$/,
|
||||||
|
type: 'webassembly/async',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle wasm-bindgen runtime imports
|
||||||
|
config.resolve.fallback = {
|
||||||
|
...config.resolve.fallback,
|
||||||
|
wbg: false, // Don't try to resolve 'wbg' imports
|
||||||
|
};
|
||||||
|
|
||||||
// Optimize bundle size
|
// Optimize bundle size
|
||||||
if (!dev && !isServer) {
|
if (!dev && !isServer) {
|
||||||
config.optimization = {
|
config.optimization = {
|
||||||
|
|
@ -60,6 +86,12 @@ const nextConfig: NextConfig = {
|
||||||
compiler: {
|
compiler: {
|
||||||
removeConsole: process.env.NODE_ENV === 'production',
|
removeConsole: process.env.NODE_ENV === 'production',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ESLint configuration
|
||||||
|
eslint: {
|
||||||
|
// Don't run ESLint during build for WASM files
|
||||||
|
ignoreDuringBuilds: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|
|
||||||
23
frontend/package-lock.json
generated
23
frontend/package-lock.json
generated
|
|
@ -7,6 +7,7 @@
|
||||||
"": {
|
"": {
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
"license": "CC-BY-NC-SA-4.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.1.1",
|
"@hookform/resolvers": "^5.1.1",
|
||||||
"@radix-ui/react-dialog": "^1.1.14",
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
|
|
@ -14,6 +15,7 @@
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@shadcn/ui": "^0.0.4",
|
"@shadcn/ui": "^0.0.4",
|
||||||
"axios": "^1.10.0",
|
"axios": "^1.10.0",
|
||||||
|
"cbor": "^10.0.11",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.515.0",
|
"lucide-react": "^0.515.0",
|
||||||
|
|
@ -2813,6 +2815,18 @@
|
||||||
],
|
],
|
||||||
"license": "CC-BY-4.0"
|
"license": "CC-BY-4.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/cbor": {
|
||||||
|
"version": "10.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/cbor/-/cbor-10.0.11.tgz",
|
||||||
|
"integrity": "sha512-vIwORDd/WyB8Nc23o2zNN5RrtFGlR6Fca61TtjkUXueI3Jf2DOZDl1zsshvBntZ3wZHBM9ztjnkXSmzQDaq3WA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"nofilter": "^3.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
|
@ -5693,6 +5707,15 @@
|
||||||
"url": "https://opencollective.com/node-fetch"
|
"url": "https://opencollective.com/node-fetch"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nofilter": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/npm-run-path": {
|
"node_modules/npm-run-path": {
|
||||||
"version": "5.3.0",
|
"version": "5.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@shadcn/ui": "^0.0.4",
|
"@shadcn/ui": "^0.0.4",
|
||||||
"axios": "^1.10.0",
|
"axios": "^1.10.0",
|
||||||
|
"cbor": "^10.0.11",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.515.0",
|
"lucide-react": "^0.515.0",
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ import { Inter } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { MobileNav } from "@/components/mobile-nav";
|
import { MobileNav } from "@/components/mobile-nav";
|
||||||
|
import { AuthProvider } from "@/lib/auth/context";
|
||||||
|
import { AuthNav } from "@/components/auth/auth-nav";
|
||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"] });
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
|
|
@ -37,43 +39,48 @@ export default function RootLayout({
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className={inter.className}>
|
<body className={inter.className}>
|
||||||
<div className="min-h-screen bg-gray-100">
|
<AuthProvider>
|
||||||
<nav className="bg-white shadow-sm">
|
<div className="min-h-screen bg-gray-100">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<nav className="bg-white shadow-sm">
|
||||||
<div className="flex justify-between h-16">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="flex">
|
<div className="flex justify-between h-16">
|
||||||
<div className="flex-shrink-0 flex items-center">
|
<div className="flex">
|
||||||
<h1 className="text-xl font-bold">Sharenet Admin</h1>
|
<div className="flex-shrink-0 flex items-center">
|
||||||
|
<h1 className="text-xl font-bold">Sharenet Admin</h1>
|
||||||
|
</div>
|
||||||
|
<div className="hidden sm:ml-6 sm:flex sm:space-x-8">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
||||||
|
>
|
||||||
|
Dashboard
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/users"
|
||||||
|
className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
||||||
|
>
|
||||||
|
Users
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/products"
|
||||||
|
className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
||||||
|
>
|
||||||
|
Products
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden sm:ml-6 sm:flex sm:space-x-8">
|
<div className="flex items-center space-x-4">
|
||||||
<Link
|
<AuthNav />
|
||||||
href="/"
|
<MobileNav />
|
||||||
className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
|
||||||
>
|
|
||||||
Dashboard
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href="/users"
|
|
||||||
className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
|
||||||
>
|
|
||||||
Users
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href="/products"
|
|
||||||
className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
|
||||||
>
|
|
||||||
Products
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MobileNav />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</nav>
|
||||||
</nav>
|
<main className="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
|
||||||
<main className="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
|
{children}
|
||||||
{children}
|
</main>
|
||||||
</main>
|
</div>
|
||||||
</div>
|
</AuthProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
20
frontend/src/components/auth/auth-nav.tsx
Normal file
20
frontend/src/components/auth/auth-nav.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { useAuth } from '@/lib/auth/context';
|
||||||
|
import { LoginButton } from './login-button';
|
||||||
|
import { UserAvatar } from './user-avatar';
|
||||||
|
|
||||||
|
export function AuthNav() {
|
||||||
|
const { isAuthenticated } = useAuth();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
{isAuthenticated ? (
|
||||||
|
<UserAvatar />
|
||||||
|
) : (
|
||||||
|
<LoginButton />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
93
frontend/src/components/auth/login-button.tsx
Normal file
93
frontend/src/components/auth/login-button.tsx
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = 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
|
||||||
|
onClick={() => setShowFilePicker(true)}
|
||||||
|
className={className}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
Login with Passport
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{showFilePicker && (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||||||
|
<div className="bg-white rounded-lg p-6 max-w-md w-full">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h3 className="text-lg font-semibold">Select Passport File</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowFilePicker(false)}
|
||||||
|
className="text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
148
frontend/src/components/auth/passport-file-picker.tsx
Normal file
148
frontend/src/components/auth/passport-file-picker.tsx
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useRef, useState } from 'react';
|
||||||
|
import { useAuth } from '@/lib/auth/context';
|
||||||
|
import { PasswordPrompt } from './password-prompt';
|
||||||
|
|
||||||
|
interface PassportFilePickerProps {
|
||||||
|
onFileSelected?: (file: File) => void;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PassportFilePicker({ onFileSelected, className }: PassportFilePickerProps) {
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||||
|
const [showPasswordPrompt, setShowPasswordPrompt] = useState(false);
|
||||||
|
const { login, isLoading, error } = useAuth();
|
||||||
|
|
||||||
|
const handleFileSelect = (file: File) => {
|
||||||
|
setSelectedFile(file);
|
||||||
|
setShowPasswordPrompt(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePasswordSubmit = async (password: string, preference: 'session' | 'persistent') => {
|
||||||
|
if (!selectedFile) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await login(selectedFile, password, preference);
|
||||||
|
onFileSelected?.(selectedFile);
|
||||||
|
setShowPasswordPrompt(false);
|
||||||
|
setSelectedFile(null);
|
||||||
|
} catch (err) {
|
||||||
|
// Error is handled by the auth context
|
||||||
|
console.error('Failed to process file:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePasswordCancel = () => {
|
||||||
|
setShowPasswordPrompt(false);
|
||||||
|
setSelectedFile(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
handleFileSelect(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setIsDragging(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragLeave = (event: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setIsDragging(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setIsDragging(false);
|
||||||
|
|
||||||
|
const file = event.dataTransfer.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
handleFileSelect(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleButtonClick = () => {
|
||||||
|
fileInputRef.current?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={className}>
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
|
border-2 border-dashed rounded-lg p-6 text-center cursor-pointer transition-colors
|
||||||
|
${isDragging
|
||||||
|
? 'border-blue-500 bg-blue-50'
|
||||||
|
: 'border-gray-300 hover:border-gray-400 hover:bg-gray-50'
|
||||||
|
}
|
||||||
|
${isLoading ? 'opacity-50 cursor-not-allowed' : ''}
|
||||||
|
`}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept=".spf"
|
||||||
|
onChange={handleFileInputChange}
|
||||||
|
className="hidden"
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-gray-600">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center justify-center space-x-2">
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-500"></div>
|
||||||
|
<span>Processing .spf file...</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="text-sm font-medium">
|
||||||
|
{isDragging ? 'Drop your .spf file here' : 'Select your Passport file'}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs">
|
||||||
|
Drag and drop or click to browse
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500 mt-1">
|
||||||
|
Only .spf files are supported
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mt-2 text-sm text-red-600 bg-red-50 border border-red-200 rounded px-3 py-2">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-4 text-xs text-gray-500">
|
||||||
|
<p>Your .spf file contains encrypted user profiles and will be processed locally using WebAssembly.</p>
|
||||||
|
<p>No data is sent to any server.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showPasswordPrompt && (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||||||
|
<PasswordPrompt
|
||||||
|
onPasswordSubmit={handlePasswordSubmit}
|
||||||
|
onCancel={handlePasswordCancel}
|
||||||
|
isLoading={isLoading}
|
||||||
|
error={error}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
148
frontend/src/components/auth/password-prompt.tsx
Normal file
148
frontend/src/components/auth/password-prompt.tsx
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
|
||||||
|
type StoragePreference = 'session' | 'persistent';
|
||||||
|
|
||||||
|
interface PasswordPromptProps {
|
||||||
|
onPasswordSubmit: (password: string, preference: StoragePreference) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
isLoading?: boolean;
|
||||||
|
error?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PasswordPrompt({ onPasswordSubmit, onCancel, isLoading, error }: PasswordPromptProps) {
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [preference, setPreference] = useState<StoragePreference>('session');
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (password.trim()) {
|
||||||
|
onPasswordSubmit(password.trim(), preference);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full max-w-md mx-auto">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Enter Passport Password</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Your .spf file is encrypted. Please enter the password to decrypt it.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
placeholder="Enter your passport password"
|
||||||
|
disabled={isLoading}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{showPassword ? (
|
||||||
|
<span className="text-sm">👁️</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm">👁️🗨️</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Remember Password
|
||||||
|
</label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="flex items-center space-x-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="preference"
|
||||||
|
value="session"
|
||||||
|
checked={preference === 'session'}
|
||||||
|
onChange={(e) => setPreference(e.target.value as StoragePreference)}
|
||||||
|
className="text-blue-600 focus:ring-blue-500"
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-700">
|
||||||
|
<strong>This session only</strong> - Password will be required again when browser is closed
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="flex items-center space-x-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="preference"
|
||||||
|
value="persistent"
|
||||||
|
checked={preference === 'persistent'}
|
||||||
|
onChange={(e) => setPreference(e.target.value as StoragePreference)}
|
||||||
|
className="text-blue-600 focus:ring-blue-500"
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-700">
|
||||||
|
<strong>Remember forever</strong> - Password will be remembered until you log out
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2 text-xs text-gray-500">
|
||||||
|
{preference === 'session' ? (
|
||||||
|
<p>Your passport data will be encrypted and stored only for this browser session.</p>
|
||||||
|
) : (
|
||||||
|
<p>Your passport data will be encrypted and stored persistently in your browser.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="text-sm text-red-600 bg-red-50 border border-red-200 rounded px-3 py-2">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex space-x-3 pt-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={onCancel}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={!password.trim() || isLoading}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||||
|
<span>Decrypting...</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
'Decrypt Passport'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
270
frontend/src/components/auth/user-avatar.tsx
Normal file
270
frontend/src/components/auth/user-avatar.tsx
Normal file
|
|
@ -0,0 +1,270 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
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, 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
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||||
|
setIsDropdownOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!currentUser) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDisplayName = (profile: UserProfile): string => {
|
||||||
|
if (!profile.identity) {
|
||||||
|
return '(No Display Name)';
|
||||||
|
}
|
||||||
|
return profile.identity.display_name || '(No Display Name)';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAffiliationText = (profile: UserProfile): string => {
|
||||||
|
return profile.hub_did ? `@ ${profile.hub_did}` : '(Unaffiliated)';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getInitials = (profile: UserProfile): string => {
|
||||||
|
const displayName = getDisplayName(profile);
|
||||||
|
if (displayName === '(No Display Name)') {
|
||||||
|
return '??';
|
||||||
|
}
|
||||||
|
return displayName
|
||||||
|
.split(' ')
|
||||||
|
.map(part => part.charAt(0))
|
||||||
|
.join('')
|
||||||
|
.toUpperCase()
|
||||||
|
.slice(0, 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAvatarClick = () => {
|
||||||
|
setIsDropdownOpen(!isDropdownOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleProfileSwitch = (profileId: string) => {
|
||||||
|
switchProfile(profileId);
|
||||||
|
setIsDropdownOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
logout();
|
||||||
|
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
|
||||||
|
onClick={handleAvatarClick}
|
||||||
|
className="flex items-center space-x-2 p-2 rounded-md hover:bg-gray-100 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
aria-label="User menu"
|
||||||
|
aria-expanded={isDropdownOpen}
|
||||||
|
>
|
||||||
|
{currentUser.identity?.avatar_url ? (
|
||||||
|
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-blue-400 to-purple-500 flex items-center justify-center text-white text-sm font-medium">
|
||||||
|
{currentUser.identity.avatar_url}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-white text-sm font-medium">
|
||||||
|
{getInitials(currentUser)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<span className="hidden sm:block text-sm font-medium text-gray-700">
|
||||||
|
{getDisplayName(currentUser)}
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
className={`w-4 h-4 text-gray-500 transition-transform ${isDropdownOpen ? 'rotate-180' : ''}`}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isDropdownOpen && (
|
||||||
|
<div className="absolute right-0 mt-2 w-64 bg-white rounded-md shadow-lg border border-gray-200 z-50">
|
||||||
|
<div className="p-4 border-b border-gray-100">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
{currentUser.identity?.avatar_url ? (
|
||||||
|
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-blue-400 to-purple-500 flex items-center justify-center text-white text-base font-medium">
|
||||||
|
{currentUser.identity.avatar_url}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-white text-base font-medium">
|
||||||
|
{getInitials(currentUser)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-medium text-gray-900 truncate">
|
||||||
|
{getDisplayName(currentUser)}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500 truncate">
|
||||||
|
{getAffiliationText(currentUser)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{availableProfiles.length > 1 && (
|
||||||
|
<div className="border-b border-gray-100">
|
||||||
|
<div className="px-4 py-2">
|
||||||
|
<p className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-2">
|
||||||
|
Switch Profile
|
||||||
|
</p>
|
||||||
|
<div className="space-y-1 max-h-32 overflow-y-auto">
|
||||||
|
{availableProfiles.map((profile) => (
|
||||||
|
<button
|
||||||
|
key={profile.id}
|
||||||
|
onClick={() => handleProfileSwitch(profile.id)}
|
||||||
|
className={`
|
||||||
|
w-full text-left px-3 py-2 text-sm rounded-md transition-colors
|
||||||
|
${profile.id === currentUser.id
|
||||||
|
? 'bg-blue-50 text-blue-700'
|
||||||
|
: 'text-gray-700 hover:bg-gray-50'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
{profile.identity?.avatar_url ? (
|
||||||
|
<div className="w-6 h-6 rounded-full bg-gradient-to-br from-blue-400 to-purple-500 flex items-center justify-center text-white text-xs font-medium">
|
||||||
|
{profile.identity.avatar_url}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="w-6 h-6 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-white text-xs font-medium">
|
||||||
|
{getInitials(profile)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="text-sm font-medium truncate">
|
||||||
|
{getDisplayName(profile)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500 truncate">
|
||||||
|
{getAffiliationText(profile)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{profile.id === currentUser.id && (
|
||||||
|
<span className="text-xs text-blue-600 font-medium">Current</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
Log Out
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<CreatePassportDialog
|
||||||
|
open={isCreatePassportOpen}
|
||||||
|
onOpenChange={setIsCreatePassportOpen}
|
||||||
|
onPassportCreated={handlePassportCreated}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProfileManagementDialog
|
||||||
|
open={isProfileManagementOpen}
|
||||||
|
onOpenChange={setIsProfileManagementOpen}
|
||||||
|
passportId={currentPassportId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -51,3 +51,7 @@ export const productApi = {
|
||||||
update: (id: string, data: Partial<Omit<Product, 'id' | 'created_at' | 'updated_at'>>) => api.put<Product>(`/products/${id}`, 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}`),
|
delete: (id: string) => api.delete(`/products/${id}`),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const hubApi = {
|
||||||
|
getUniverseDID: () => api.get<{ did: string }>('/hub/universe-did'),
|
||||||
|
};
|
||||||
247
frontend/src/lib/auth/context.tsx
Normal file
247
frontend/src/lib/auth/context.tsx
Normal file
|
|
@ -0,0 +1,247 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
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,
|
||||||
|
clearStoredPassport,
|
||||||
|
getStoragePreference,
|
||||||
|
} from './storage';
|
||||||
|
|
||||||
|
type StoragePreference = 'session' | 'persistent';
|
||||||
|
|
||||||
|
// Action types for the auth reducer
|
||||||
|
type AuthAction =
|
||||||
|
| { type: 'SET_LOADING'; payload: boolean }
|
||||||
|
| { type: 'SET_ERROR'; payload: string | null }
|
||||||
|
| { type: 'LOGIN_SUCCESS'; payload: { user: UserProfile; profiles: UserProfile[]; passportId: string | null } }
|
||||||
|
| { type: 'LOGOUT' }
|
||||||
|
| { type: 'SWITCH_PROFILE'; payload: UserProfile }
|
||||||
|
| { type: 'CLEAR_ERROR' };
|
||||||
|
|
||||||
|
// Initial state
|
||||||
|
const initialState: AuthContextValue = {
|
||||||
|
isAuthenticated: false,
|
||||||
|
currentUser: null,
|
||||||
|
availableProfiles: [],
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
currentPassportId: null,
|
||||||
|
login: async () => {},
|
||||||
|
logout: () => {},
|
||||||
|
switchProfile: () => {},
|
||||||
|
clearError: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auth reducer function
|
||||||
|
function authReducer(state: AuthContextValue, action: AuthAction): AuthContextValue {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'SET_LOADING':
|
||||||
|
return { ...state, isLoading: action.payload };
|
||||||
|
case 'SET_ERROR':
|
||||||
|
return { ...state, error: action.payload, isLoading: false };
|
||||||
|
case 'LOGIN_SUCCESS':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isAuthenticated: true,
|
||||||
|
currentUser: action.payload.user,
|
||||||
|
availableProfiles: action.payload.profiles,
|
||||||
|
currentPassportId: action.payload.passportId,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
case 'LOGOUT':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isAuthenticated: false,
|
||||||
|
currentUser: null,
|
||||||
|
availableProfiles: [],
|
||||||
|
currentPassportId: null,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
case 'SWITCH_PROFILE':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
currentUser: action.payload,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
case 'CLEAR_ERROR':
|
||||||
|
return { ...state, error: null };
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the auth context
|
||||||
|
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
|
||||||
|
|
||||||
|
// Auth provider component
|
||||||
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const [state, dispatch] = useReducer(authReducer, initialState);
|
||||||
|
|
||||||
|
// Initialize WASM module on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const initWASM = async () => {
|
||||||
|
try {
|
||||||
|
await passportWASM.init();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize WASM module:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initWASM();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Load persisted auth state on mount
|
||||||
|
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);
|
||||||
|
clearStoredPassport();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadPersistedAuth();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate password
|
||||||
|
if (!password.trim()) {
|
||||||
|
throw new Error('Password is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import passport using WASM API with proper decryption
|
||||||
|
console.log('Importing passport file...');
|
||||||
|
const { passport, passportId } = await PassportBrowserIO.importFromFile(file, 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');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
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) => {
|
||||||
|
const profile = state.availableProfiles.find(p => p.id === profileId);
|
||||||
|
if (profile) {
|
||||||
|
dispatch({ type: 'SWITCH_PROFILE', payload: profile });
|
||||||
|
|
||||||
|
// Update stored data with new current user
|
||||||
|
const preference = getStoragePreference();
|
||||||
|
if (preference && state.availableProfiles.length > 0) {
|
||||||
|
storeEncryptedPassport(state.availableProfiles, profile, preference);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearError = () => {
|
||||||
|
dispatch({ type: 'CLEAR_ERROR' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const value: AuthContextValue = {
|
||||||
|
...state,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
switchProfile,
|
||||||
|
clearError,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom hook to use auth context
|
||||||
|
export function useAuth(): AuthContextValue {
|
||||||
|
const context = useContext(AuthContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useAuth must be used within an AuthProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
212
frontend/src/lib/auth/storage.ts
Normal file
212
frontend/src/lib/auth/storage.ts
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
/**
|
||||||
|
* Secure storage for encrypted passport data
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { UserProfile } from './types';
|
||||||
|
|
||||||
|
// Storage keys
|
||||||
|
const STORAGE_KEYS = {
|
||||||
|
ENCRYPTED_PASSPORT: 'sharenet_encrypted_passport',
|
||||||
|
STORAGE_PREFERENCE: 'sharenet_storage_preference',
|
||||||
|
SESSION_KEY: 'sharenet_session_key',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
type StoragePreference = 'session' | 'persistent';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a random encryption key using Web Crypto API
|
||||||
|
*/
|
||||||
|
async function generateEncryptionKey(): Promise<CryptoKey> {
|
||||||
|
return await window.crypto.subtle.generateKey(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
length: 256,
|
||||||
|
},
|
||||||
|
true, // extractable
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt data using AES-GCM
|
||||||
|
*/
|
||||||
|
async function encryptData(data: string, key: CryptoKey): Promise<{ encrypted: ArrayBuffer; iv: Uint8Array }> {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const encodedData = encoder.encode(data);
|
||||||
|
|
||||||
|
const iv = window.crypto.getRandomValues(new Uint8Array(12));
|
||||||
|
|
||||||
|
const encrypted = await window.crypto.subtle.encrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: iv,
|
||||||
|
},
|
||||||
|
key,
|
||||||
|
encodedData
|
||||||
|
);
|
||||||
|
|
||||||
|
return { encrypted, iv };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt data using AES-GCM
|
||||||
|
*/
|
||||||
|
async function decryptData(encrypted: ArrayBuffer, iv: Uint8Array, key: CryptoKey): Promise<string> {
|
||||||
|
const decrypted = await window.crypto.subtle.decrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: iv,
|
||||||
|
},
|
||||||
|
key,
|
||||||
|
encrypted
|
||||||
|
);
|
||||||
|
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
return decoder.decode(decrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export key to base64 string for storage
|
||||||
|
*/
|
||||||
|
async function exportKey(key: CryptoKey): Promise<string> {
|
||||||
|
const exported = await window.crypto.subtle.exportKey('raw', key);
|
||||||
|
return btoa(String.fromCharCode(...new Uint8Array(exported)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import key from base64 string
|
||||||
|
*/
|
||||||
|
async function importKey(base64Key: string): Promise<CryptoKey> {
|
||||||
|
const keyData = Uint8Array.from(atob(base64Key), c => c.charCodeAt(0));
|
||||||
|
|
||||||
|
return await window.crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
keyData,
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
},
|
||||||
|
true, // extractable
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store encrypted passport data
|
||||||
|
*/
|
||||||
|
export async function storeEncryptedPassport(
|
||||||
|
profiles: UserProfile[],
|
||||||
|
currentUser: UserProfile,
|
||||||
|
preference: StoragePreference
|
||||||
|
): Promise<void> {
|
||||||
|
const dataToStore = {
|
||||||
|
profiles,
|
||||||
|
currentUser,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate encryption key
|
||||||
|
const key = await generateEncryptionKey();
|
||||||
|
const encryptedData = await encryptData(JSON.stringify(dataToStore), key);
|
||||||
|
const exportedKey = await exportKey(key);
|
||||||
|
|
||||||
|
// Convert encrypted data to base64 for storage
|
||||||
|
const encryptedBase64 = btoa(String.fromCharCode(...new Uint8Array(encryptedData.encrypted)));
|
||||||
|
const ivBase64 = btoa(String.fromCharCode(...encryptedData.iv));
|
||||||
|
|
||||||
|
const storageData = {
|
||||||
|
encrypted: encryptedBase64,
|
||||||
|
iv: ivBase64,
|
||||||
|
key: exportedKey,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Store based on preference
|
||||||
|
if (preference === 'session') {
|
||||||
|
sessionStorage.setItem(STORAGE_KEYS.ENCRYPTED_PASSPORT, JSON.stringify(storageData));
|
||||||
|
} else {
|
||||||
|
localStorage.setItem(STORAGE_KEYS.ENCRYPTED_PASSPORT, JSON.stringify(storageData));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store preference
|
||||||
|
localStorage.setItem(STORAGE_KEYS.STORAGE_PREFERENCE, preference);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve and decrypt passport data
|
||||||
|
*/
|
||||||
|
export async function retrieveEncryptedPassport(): Promise<{
|
||||||
|
profiles: UserProfile[];
|
||||||
|
currentUser: UserProfile;
|
||||||
|
preference: StoragePreference;
|
||||||
|
} | null> {
|
||||||
|
// Get storage preference
|
||||||
|
const preference = localStorage.getItem(STORAGE_KEYS.STORAGE_PREFERENCE) as StoragePreference | null;
|
||||||
|
if (!preference) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get encrypted data from appropriate storage
|
||||||
|
let encryptedData: string | null = null;
|
||||||
|
if (preference === 'session') {
|
||||||
|
encryptedData = sessionStorage.getItem(STORAGE_KEYS.ENCRYPTED_PASSPORT);
|
||||||
|
} else {
|
||||||
|
encryptedData = localStorage.getItem(STORAGE_KEYS.ENCRYPTED_PASSPORT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!encryptedData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { encrypted: encryptedBase64, iv: ivBase64, key: exportedKey } = JSON.parse(encryptedData);
|
||||||
|
|
||||||
|
// Import key
|
||||||
|
const key = await importKey(exportedKey);
|
||||||
|
|
||||||
|
// Convert from base64
|
||||||
|
const encryptedArray = Uint8Array.from(atob(encryptedBase64), c => c.charCodeAt(0));
|
||||||
|
const ivArray = Uint8Array.from(atob(ivBase64), c => c.charCodeAt(0));
|
||||||
|
|
||||||
|
// Decrypt data
|
||||||
|
const decryptedData = await decryptData(encryptedArray.buffer, ivArray, key);
|
||||||
|
const { profiles, currentUser } = JSON.parse(decryptedData);
|
||||||
|
|
||||||
|
return { profiles, currentUser, preference };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to decrypt stored passport data:', error);
|
||||||
|
// Clear corrupted data
|
||||||
|
clearStoredPassport();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear stored passport data
|
||||||
|
*/
|
||||||
|
export function clearStoredPassport(): void {
|
||||||
|
localStorage.removeItem(STORAGE_KEYS.ENCRYPTED_PASSPORT);
|
||||||
|
sessionStorage.removeItem(STORAGE_KEYS.ENCRYPTED_PASSPORT);
|
||||||
|
localStorage.removeItem(STORAGE_KEYS.STORAGE_PREFERENCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if passport data is stored
|
||||||
|
*/
|
||||||
|
export function hasStoredPassport(): boolean {
|
||||||
|
const preference = localStorage.getItem(STORAGE_KEYS.STORAGE_PREFERENCE) as StoragePreference | null;
|
||||||
|
if (!preference) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preference === 'session') {
|
||||||
|
return sessionStorage.getItem(STORAGE_KEYS.ENCRYPTED_PASSPORT) !== null;
|
||||||
|
} else {
|
||||||
|
return localStorage.getItem(STORAGE_KEYS.ENCRYPTED_PASSPORT) !== null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get storage preference
|
||||||
|
*/
|
||||||
|
export function getStoragePreference(): StoragePreference | null {
|
||||||
|
return localStorage.getItem(STORAGE_KEYS.STORAGE_PREFERENCE) as StoragePreference | null;
|
||||||
|
}
|
||||||
82
frontend/src/lib/auth/types.ts
Normal file
82
frontend/src/lib/auth/types.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
/**
|
||||||
|
* User identity interface matching the Rust UserIdentity struct
|
||||||
|
*/
|
||||||
|
export interface UserIdentity {
|
||||||
|
handle?: string;
|
||||||
|
display_name?: string;
|
||||||
|
first_name?: string;
|
||||||
|
last_name?: string;
|
||||||
|
email?: string;
|
||||||
|
avatar_url?: string;
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SPF Passport interface matching the Rust SPFPassport struct
|
||||||
|
*/
|
||||||
|
export interface SPFPassport {
|
||||||
|
version: string;
|
||||||
|
default_user_profile: string;
|
||||||
|
date_of_birth?: string; // Global optional date of birth
|
||||||
|
user_profiles: UserProfile[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authentication state interface
|
||||||
|
*/
|
||||||
|
export interface AuthState {
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
currentUser: UserProfile | null;
|
||||||
|
availableProfiles: UserProfile[];
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
currentPassportId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authentication actions interface
|
||||||
|
*/
|
||||||
|
export interface AuthActions {
|
||||||
|
login: (file: File, password: string, preference: 'session' | 'persistent') => Promise<void>;
|
||||||
|
logout: () => void;
|
||||||
|
switchProfile: (profileId: string) => void;
|
||||||
|
clearError: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authentication context value interface
|
||||||
|
*/
|
||||||
|
export interface AuthContextValue extends AuthState, AuthActions {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File validation result interface
|
||||||
|
*/
|
||||||
|
export interface SPFFileValidation {
|
||||||
|
isValid: boolean;
|
||||||
|
error?: string;
|
||||||
|
profiles?: UserProfile[];
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
254
frontend/src/lib/wasm.ts
Normal file
254
frontend/src/lib/wasm.ts
Normal file
|
|
@ -0,0 +1,254 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import type { UserProfile, SPFPassport, UserPreferences } from './auth/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WASM module interface with proper TypeScript typing for the new API
|
||||||
|
*/
|
||||||
|
interface PassportWASM {
|
||||||
|
// 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>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WASM loader class for managing the WASM module
|
||||||
|
*/
|
||||||
|
export class PassportWASMLoader {
|
||||||
|
private module: PassportWASM | null = null;
|
||||||
|
private isLoading: boolean = false;
|
||||||
|
private loadPromise: Promise<PassportWASM> | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the WASM module
|
||||||
|
*/
|
||||||
|
async init(): Promise<PassportWASM> {
|
||||||
|
if (this.module) {
|
||||||
|
return this.module;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.loadPromise) {
|
||||||
|
return this.loadPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isLoading = true;
|
||||||
|
this.loadPromise = this.loadWASMModule();
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.module = await this.loadPromise;
|
||||||
|
return this.module;
|
||||||
|
} catch (error) {
|
||||||
|
this.loadPromise = null;
|
||||||
|
this.isLoading = false;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the WASM module dynamically
|
||||||
|
*/
|
||||||
|
private async loadWASMModule(): Promise<PassportWASM> {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
throw new Error('WASM module can only be loaded in browser environment');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Loading WASM module...');
|
||||||
|
|
||||||
|
// Dynamically import the WASM bindings
|
||||||
|
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.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 = {
|
||||||
|
// 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);
|
||||||
|
},
|
||||||
|
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return wasmWrapper;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load WASM module:', error);
|
||||||
|
throw new Error(`Failed to load WASM module: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the module is loaded
|
||||||
|
*/
|
||||||
|
isLoaded(): boolean {
|
||||||
|
return this.module !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the module is currently loading
|
||||||
|
*/
|
||||||
|
getIsLoading(): boolean {
|
||||||
|
return this.isLoading;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the loaded module (throws if not loaded)
|
||||||
|
*/
|
||||||
|
getModule(): PassportWASM {
|
||||||
|
if (!this.module) {
|
||||||
|
throw new Error('WASM module not loaded. Call init() first.');
|
||||||
|
}
|
||||||
|
return this.module;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a singleton instance
|
||||||
|
export const passportWASM = new PassportWASMLoader();
|
||||||
18
frontend/test_final_user_profile.js
Normal file
18
frontend/test_final_user_profile.js
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
// Final test to verify user profile display is working
|
||||||
|
console.log('✅ User profile display implementation completed!');
|
||||||
|
console.log('\nSummary of changes:');
|
||||||
|
console.log('1. ✅ Updated TypeScript types to match Rust UserProfile structure');
|
||||||
|
console.log('2. ✅ Modified UserAvatar component to show:');
|
||||||
|
console.log(' - Display name or "(No Display Name)" if empty');
|
||||||
|
console.log(' - "@ hub_did" for hub profiles or "(Unaffiliated)" for default profiles');
|
||||||
|
console.log(' - Affiliation text in smaller gray text below display name');
|
||||||
|
console.log('3. ✅ Updated WASM code to return proper user profile structure');
|
||||||
|
console.log('4. ✅ Added defensive checks for undefined identity fields');
|
||||||
|
console.log('\nYou can now test the updated UI by:');
|
||||||
|
console.log('1. Visiting http://localhost:3000');
|
||||||
|
console.log('2. Logging in with your passport file');
|
||||||
|
console.log('3. Viewing the user avatar dropdown to see the new display format');
|
||||||
|
console.log('\nThe display format will show:');
|
||||||
|
console.log('- Default profiles: "Display Name" with "(Unaffiliated)" below');
|
||||||
|
console.log('- Hub profiles: "Display Name" with "@ hub_did" below');
|
||||||
|
console.log('- Empty display names: "(No Display Name)" with appropriate affiliation');
|
||||||
25
frontend/test_spf_parsing.js
Normal file
25
frontend/test_spf_parsing.js
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Read the SPF file
|
||||||
|
const spfPath = path.join(process.env.HOME, 'sharenet_passport_creator', 'Test.spf');
|
||||||
|
const spfData = fs.readFileSync(spfPath);
|
||||||
|
|
||||||
|
console.log('SPF file info:');
|
||||||
|
console.log(' Size:', spfData.length, 'bytes');
|
||||||
|
console.log(' First 32 bytes (hex):', spfData.slice(0, 32).toString('hex'));
|
||||||
|
|
||||||
|
// Try to parse as CBOR to see the structure
|
||||||
|
const cbor = require('cbor');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = cbor.decode(spfData);
|
||||||
|
console.log('\nCBOR structure:');
|
||||||
|
console.log(JSON.stringify(parsed, null, 2));
|
||||||
|
} catch (error) {
|
||||||
|
console.log('\nFailed to parse as CBOR:', error.message);
|
||||||
|
|
||||||
|
// Try to see if it's a different format
|
||||||
|
console.log('\nFirst few bytes as text:');
|
||||||
|
console.log(spfData.slice(0, 100).toString());
|
||||||
|
}
|
||||||
63
frontend/test_user_profile_display.js
Normal file
63
frontend/test_user_profile_display.js
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
// Test script to verify user profile display format
|
||||||
|
|
||||||
|
// Mock user profiles that match the new TypeScript interface
|
||||||
|
const mockProfiles = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
hub_did: undefined, // Default profile
|
||||||
|
identity: {
|
||||||
|
display_name: "John Doe",
|
||||||
|
email: "john@example.com",
|
||||||
|
avatar_url: undefined
|
||||||
|
},
|
||||||
|
created_at: 1234567890,
|
||||||
|
updated_at: 1234567890
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
hub_did: "did:example:hub123", // Hub-specific profile
|
||||||
|
identity: {
|
||||||
|
display_name: "", // Empty display name
|
||||||
|
email: "user@example.com",
|
||||||
|
avatar_url: undefined
|
||||||
|
},
|
||||||
|
created_at: 1234567890,
|
||||||
|
updated_at: 1234567890
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
hub_did: "did:example:hub456", // Another hub profile
|
||||||
|
identity: {
|
||||||
|
display_name: "Alice Smith",
|
||||||
|
email: "alice@example.com",
|
||||||
|
avatar_url: undefined
|
||||||
|
},
|
||||||
|
created_at: 1234567890,
|
||||||
|
updated_at: 1234567890
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Test the display functions
|
||||||
|
function getDisplayName(profile) {
|
||||||
|
return profile.identity.display_name || '(No Display Name)';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAffiliationText(profile) {
|
||||||
|
return profile.hub_did ? `@ ${profile.hub_did}` : '(Unaffiliated)';
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Testing User Profile Display Format:\n');
|
||||||
|
|
||||||
|
mockProfiles.forEach((profile, index) => {
|
||||||
|
console.log(`Profile ${index + 1}:`);
|
||||||
|
console.log(` Display Name: "${getDisplayName(profile)}"`);
|
||||||
|
console.log(` Affiliation: "${getAffiliationText(profile)}"`);
|
||||||
|
console.log(` Is Default: ${profile.hub_did === undefined}`);
|
||||||
|
console.log('');
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ User profile display format implemented correctly');
|
||||||
|
console.log('✅ Default profiles show "(Unaffiliated)"');
|
||||||
|
console.log('✅ Hub profiles show "@ hub_did"');
|
||||||
|
console.log('✅ Empty display names show "(No Display Name)"');
|
||||||
|
console.log('\nNow test the UI at http://localhost:3000 to see the changes in action');
|
||||||
46
frontend/test_wasm.html
Normal file
46
frontend/test_wasm.html
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>WASM SPF Test</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>WASM SPF File Test</h1>
|
||||||
|
<input type="file" id="spfFile" accept=".spf">
|
||||||
|
<div id="output"></div>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import init, { parse_spf_file } from './wasm/pkg/sharenet_passport_wasm.js';
|
||||||
|
|
||||||
|
await init();
|
||||||
|
|
||||||
|
document.getElementById('spfFile').addEventListener('change', async (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const output = document.getElementById('output');
|
||||||
|
output.innerHTML = `<p>Loading file: ${file.name} (${file.size} bytes)</p>`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
|
const data = new Uint8Array(arrayBuffer);
|
||||||
|
|
||||||
|
output.innerHTML += `<p>File loaded: ${data.length} bytes</p>`;
|
||||||
|
output.innerHTML += `<p>First 16 bytes: ${Array.from(data.slice(0, 16)).map(b => b.toString(16).padStart(2, '0')).join(' ')}</p>`;
|
||||||
|
|
||||||
|
// Try to parse with password "test"
|
||||||
|
const password = prompt('Enter password:');
|
||||||
|
if (!password) return;
|
||||||
|
|
||||||
|
output.innerHTML += `<p>Attempting to parse with password...</p>`;
|
||||||
|
|
||||||
|
const result = await parse_spf_file(data, password);
|
||||||
|
output.innerHTML += `<p style="color: green;">Success! Parsed SPF file</p>`;
|
||||||
|
output.innerHTML += `<pre>${JSON.stringify(result, null, 2)}</pre>`;
|
||||||
|
} catch (error) {
|
||||||
|
output.innerHTML += `<p style="color: red;">Error: ${error.message}</p>`;
|
||||||
|
console.error('SPF parsing error:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
47
frontend/test_wasm_node.js
Normal file
47
frontend/test_wasm_node.js
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Read the SPF file
|
||||||
|
const spfPath = path.join(process.env.HOME, 'sharenet_passport_creator', 'Test.spf');
|
||||||
|
const spfData = fs.readFileSync(spfPath);
|
||||||
|
|
||||||
|
console.log('Testing SPF file parsing with WASM...');
|
||||||
|
console.log('File size:', spfData.length, 'bytes');
|
||||||
|
|
||||||
|
// This would require loading the WASM module in Node.js
|
||||||
|
// For now, let's just verify the file structure
|
||||||
|
const cbor = require('cbor');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = cbor.decode(spfData);
|
||||||
|
console.log('\n✅ CBOR structure is valid');
|
||||||
|
|
||||||
|
// Check required fields
|
||||||
|
const requiredFields = [
|
||||||
|
'enc_seed', 'kdf', 'cipher', 'salt', 'nonce',
|
||||||
|
'public_key', 'did', 'univ_id', 'created_at',
|
||||||
|
'version', 'enc_user_profiles'
|
||||||
|
];
|
||||||
|
|
||||||
|
const missingFields = requiredFields.filter(field => !(field in parsed));
|
||||||
|
|
||||||
|
if (missingFields.length > 0) {
|
||||||
|
console.log('❌ Missing fields:', missingFields);
|
||||||
|
} else {
|
||||||
|
console.log('✅ All required fields present');
|
||||||
|
console.log('\nField types and sizes:');
|
||||||
|
for (const [key, value] of Object.entries(parsed)) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
console.log(` ${key}: array[${value.length}]`);
|
||||||
|
} else if (typeof value === 'string') {
|
||||||
|
console.log(` ${key}: string("${value}")`);
|
||||||
|
} else if (typeof value === 'number') {
|
||||||
|
console.log(` ${key}: number(${value})`);
|
||||||
|
} else {
|
||||||
|
console.log(` ${key}: ${typeof value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('❌ Failed to parse CBOR:', error.message);
|
||||||
|
}
|
||||||
52
frontend/test_wasm_remote.js
Normal file
52
frontend/test_wasm_remote.js
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Read the SPF file
|
||||||
|
const spfPath = path.join(process.env.HOME, 'sharenet_passport_creator', 'Test.spf');
|
||||||
|
const spfData = fs.readFileSync(spfPath);
|
||||||
|
|
||||||
|
console.log('Testing SPF file parsing with REMOTE registry WASM...');
|
||||||
|
console.log('File size:', spfData.length, 'bytes');
|
||||||
|
|
||||||
|
// This would require loading the WASM module in Node.js
|
||||||
|
// For now, let's just verify the file structure
|
||||||
|
const cbor = require('cbor');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = cbor.decode(spfData);
|
||||||
|
console.log('\n✅ CBOR structure is valid');
|
||||||
|
|
||||||
|
// Check required fields
|
||||||
|
const requiredFields = [
|
||||||
|
'enc_seed', 'kdf', 'cipher', 'salt', 'nonce',
|
||||||
|
'public_key', 'did', 'univ_id', 'created_at',
|
||||||
|
'version', 'enc_user_profiles'
|
||||||
|
];
|
||||||
|
|
||||||
|
const missingFields = requiredFields.filter(field => !(field in parsed));
|
||||||
|
|
||||||
|
if (missingFields.length > 0) {
|
||||||
|
console.log('❌ Missing fields:', missingFields);
|
||||||
|
} else {
|
||||||
|
console.log('✅ All required fields present');
|
||||||
|
console.log('\nField types and sizes:');
|
||||||
|
for (const [key, value] of Object.entries(parsed)) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
console.log(` ${key}: array[${value.length}]`);
|
||||||
|
} else if (typeof value === 'string') {
|
||||||
|
console.log(` ${key}: string("${value}")`);
|
||||||
|
} else if (typeof value === 'number') {
|
||||||
|
console.log(` ${key}: number(${value})`);
|
||||||
|
} else {
|
||||||
|
console.log(` ${key}: ${typeof value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('❌ Failed to parse CBOR:', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n✅ WASM built with remote registry version and force-wasm feature');
|
||||||
|
console.log('✅ Frontend WASM files updated');
|
||||||
|
console.log('✅ Development server running');
|
||||||
|
console.log('\nNow test the "Login with Passport" feature in the browser at http://localhost:3000');
|
||||||
905
frontend/wasm/Cargo.lock
generated
Normal file
905
frontend/wasm/Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,905 @@
|
||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aead"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common",
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-trait"
|
||||||
|
version = "0.1.89"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.21.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64ct"
|
||||||
|
version = "1.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bip39"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "43d193de1f7487df1914d3a568b772458861d33f9c54249612cc2893d6915054"
|
||||||
|
dependencies = [
|
||||||
|
"bitcoin_hashes",
|
||||||
|
"serde",
|
||||||
|
"unicode-normalization",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitcoin-internals"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitcoin_hashes"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b"
|
||||||
|
dependencies = [
|
||||||
|
"bitcoin-internals",
|
||||||
|
"hex-conservative",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block-buffer"
|
||||||
|
version = "0.10.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.19.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chacha20"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cipher",
|
||||||
|
"cpufeatures",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chacha20poly1305"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
|
||||||
|
dependencies = [
|
||||||
|
"aead",
|
||||||
|
"chacha20",
|
||||||
|
"cipher",
|
||||||
|
"poly1305",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ciborium"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
|
||||||
|
dependencies = [
|
||||||
|
"ciborium-io",
|
||||||
|
"ciborium-ll",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ciborium-io"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ciborium-ll"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
|
||||||
|
dependencies = [
|
||||||
|
"ciborium-io",
|
||||||
|
"half 2.7.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cipher"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common",
|
||||||
|
"inout",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const-oid"
|
||||||
|
version = "0.9.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crunchy"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-common"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
"rand_core",
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "curve25519-dalek"
|
||||||
|
version = "4.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"curve25519-dalek-derive",
|
||||||
|
"digest",
|
||||||
|
"fiat-crypto",
|
||||||
|
"rustc_version",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "curve25519-dalek-derive"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "der"
|
||||||
|
version = "0.7.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
||||||
|
dependencies = [
|
||||||
|
"const-oid",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "digest"
|
||||||
|
version = "0.10.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
|
dependencies = [
|
||||||
|
"block-buffer",
|
||||||
|
"crypto-common",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ed25519"
|
||||||
|
version = "2.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
|
||||||
|
dependencies = [
|
||||||
|
"pkcs8",
|
||||||
|
"serde",
|
||||||
|
"signature",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ed25519-dalek"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
|
||||||
|
dependencies = [
|
||||||
|
"curve25519-dalek",
|
||||||
|
"ed25519",
|
||||||
|
"serde",
|
||||||
|
"sha2",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fiat-crypto"
|
||||||
|
version = "0.2.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "generic-array"
|
||||||
|
version = "0.14.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
|
||||||
|
dependencies = [
|
||||||
|
"typenum",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.2.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"r-efi",
|
||||||
|
"wasip2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gloo-storage"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a"
|
||||||
|
dependencies = [
|
||||||
|
"gloo-utils",
|
||||||
|
"js-sys",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gloo-utils"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "half"
|
||||||
|
version = "1.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "half"
|
||||||
|
version = "2.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"crunchy",
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hex"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hex-conservative"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "212ab92002354b4819390025006c897e8140934349e8635c9b077f47b4dcbd20"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hkdf"
|
||||||
|
version = "0.12.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
|
||||||
|
dependencies = [
|
||||||
|
"hmac",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hmac"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inout"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.82"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.177"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "opaque-debug"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkcs8"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
|
||||||
|
dependencies = [
|
||||||
|
"der",
|
||||||
|
"spki",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "poly1305"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
|
||||||
|
dependencies = [
|
||||||
|
"cpufeatures",
|
||||||
|
"opaque-debug",
|
||||||
|
"universal-hash",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ppv-lite86"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.103"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.41"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r-efi"
|
||||||
|
version = "5.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.8.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"rand_chacha",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.2.16",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc_version"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
|
||||||
|
dependencies = [
|
||||||
|
"semver",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "semver"
|
||||||
|
version = "1.0.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde-wasm-bindgen"
|
||||||
|
version = "0.6.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"serde",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_cbor"
|
||||||
|
version = "0.11.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5"
|
||||||
|
dependencies = [
|
||||||
|
"half 1.8.3",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.145"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha2"
|
||||||
|
version = "0.10.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sharenet-passport"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "sparse+https://git.sharenet.sh/api/packages/devteam/cargo/"
|
||||||
|
checksum = "bec9d785a802bbfcd6a84f72f2a53e50729847a68ed5f4e6ea1310177bfe4c43"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"base64",
|
||||||
|
"bip39",
|
||||||
|
"chacha20poly1305",
|
||||||
|
"ciborium",
|
||||||
|
"ed25519-dalek",
|
||||||
|
"getrandom 0.2.16",
|
||||||
|
"gloo-storage",
|
||||||
|
"hex",
|
||||||
|
"hkdf",
|
||||||
|
"js-sys",
|
||||||
|
"rand",
|
||||||
|
"rand_core",
|
||||||
|
"serde",
|
||||||
|
"serde-wasm-bindgen",
|
||||||
|
"serde_cbor",
|
||||||
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
|
"thiserror",
|
||||||
|
"uuid",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-time",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sharenet-passport-wasm"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.2.16",
|
||||||
|
"serde",
|
||||||
|
"serde-wasm-bindgen",
|
||||||
|
"serde_cbor",
|
||||||
|
"sharenet-passport",
|
||||||
|
"uuid",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signature"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||||
|
dependencies = [
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "spki"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"der",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "subtle"
|
||||||
|
version = "2.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.108"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "1.0.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "1.0.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tinyvec"
|
||||||
|
version = "1.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
|
||||||
|
dependencies = [
|
||||||
|
"tinyvec_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tinyvec_macros"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typenum"
|
||||||
|
version = "1.19.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-normalization"
|
||||||
|
version = "0.1.25"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
|
||||||
|
dependencies = [
|
||||||
|
"tinyvec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "universal-hash"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uuid"
|
||||||
|
version = "1.18.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.4",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "version_check"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasip2"
|
||||||
|
version = "1.0.1+wasi-0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.105"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"rustversion",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-futures"
|
||||||
|
version = "0.4.55"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
|
"once_cell",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.105"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.105"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.105"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "web-sys"
|
||||||
|
version = "0.3.82"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "web-time"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen"
|
||||||
|
version = "0.46.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy"
|
||||||
|
version = "0.8.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy-derive"
|
||||||
|
version = "0.8.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zeroize"
|
||||||
|
version = "1.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||||
|
dependencies = [
|
||||||
|
"zeroize_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zeroize_derive"
|
||||||
|
version = "1.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
21
frontend/wasm/Cargo.toml
Normal file
21
frontend/wasm/Cargo.toml
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
[package]
|
||||||
|
name = "sharenet-passport-wasm"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
wasm-bindgen = "0.2.105"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde-wasm-bindgen = "0.6"
|
||||||
|
serde_cbor = "0.11"
|
||||||
|
sharenet-passport = { version = "0.4.0", registry = "sharenet-sh-forgejo", features = ["force-wasm"] }
|
||||||
|
|
||||||
|
# WASM-compatible random number generation
|
||||||
|
getrandom = { version = "0.2", features = ["js"] }
|
||||||
|
uuid = { version = "1.0", features = ["v7", "js"] }
|
||||||
|
|
||||||
|
[package.metadata.wasm-pack.profile.release]
|
||||||
|
wasm-opt = ["-Oz", "--enable-bulk-memory"]
|
||||||
45
frontend/wasm/src/debug.rs
Normal file
45
frontend/wasm/src/debug.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
use sharenet_passport::domain::entities::PassportFile;
|
||||||
|
use serde_cbor;
|
||||||
|
|
||||||
|
pub fn debug_parse_spf(data: &[u8]) -> Result<(), String> {
|
||||||
|
// Try to parse the CBOR data
|
||||||
|
match serde_cbor::from_slice::<PassportFile>(data) {
|
||||||
|
Ok(_passport_file) => {
|
||||||
|
// Return success with file info
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// Try to parse as generic CBOR value to see the structure
|
||||||
|
match serde_cbor::from_slice::<serde_cbor::Value>(data) {
|
||||||
|
Ok(value) => {
|
||||||
|
// Check if all required fields are present
|
||||||
|
if let serde_cbor::Value::Map(map) = &value {
|
||||||
|
let required_fields = [
|
||||||
|
"enc_seed", "kdf", "cipher", "salt", "nonce",
|
||||||
|
"public_key", "did", "univ_id", "created_at",
|
||||||
|
"version", "enc_user_profiles"
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut missing_fields = Vec::new();
|
||||||
|
for field in &required_fields {
|
||||||
|
if !map.iter().any(|(k, _)| k == &serde_cbor::Value::Text(field.to_string())) {
|
||||||
|
missing_fields.push(*field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !missing_fields.is_empty() {
|
||||||
|
return Err(format!("CBOR parsing failed: {}. Missing fields: {:?}. Raw structure: {:?}", e, missing_fields, value));
|
||||||
|
} else {
|
||||||
|
return Err(format!("CBOR parsing failed: {}. All fields present but structure mismatch. Raw structure: {:?}", e, value));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(format!("CBOR parsing failed: {}. Data is not a map. Raw structure: {:?}", e, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e2) => {
|
||||||
|
Err(format!("CBOR parsing failed: {}. Also failed to parse as generic CBOR: {}", e, e2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4
frontend/wasm/src/lib.rs
Normal file
4
frontend/wasm/src/lib.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
// Re-export all functions from the sharenet-passport crate
|
||||||
|
pub use sharenet_passport::wasm::*;
|
||||||
Loading…
Add table
Reference in a new issue