diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml
index a34e2f2..4dc9f96 100644
--- a/.forgejo/workflows/ci.yml
+++ b/.forgejo/workflows/ci.yml
@@ -140,7 +140,7 @@ jobs:
build-backend:
runs-on: [ci]
- # if: false
+ if: false
# needs: [test-backend, test-frontend]
# needs: [test-frontend]
@@ -176,7 +176,7 @@ jobs:
build-frontend:
runs-on: [ci]
- if: false
+ # if: false
# needs: [test-backend, test-frontend]
# needs: [test-frontend]
@@ -208,6 +208,7 @@ jobs:
echo "=== Inode info ==="
df -i /home/ci-service /tmp /var/tmp 2>/dev/null || df -i /tmp /var/tmp
+
- name: Build frontend container image
run: |
# Create temp directory on larger filesystem
@@ -236,8 +237,8 @@ jobs:
deploy-prod:
runs-on: [prod]
- needs: [build-backend]
- # needs: [build-frontend]
+ # needs: [build-backend]
+ needs: [build-frontend]
# needs: [build-backend, build-frontend]
env:
@@ -292,19 +293,19 @@ jobs:
docker.io/nginx:alpine \
sh -lc 'nginx -t -c /etc/nginx/nginx.conf'
- # APPLY/RE-APPLY THE POD (no explicit "down"; use --replace)
- - name: Apply pod (kube play --replace)
+ # If --replace is NOT supported in your Podman, use this fallback instead:
+ - name: Recreate pod (fallback)
run: |
set -euo pipefail
- # If your Podman supports --replace, this is the cleanest:
- envsubst < deploy/prod-pod.yml | podman --remote kube play --replace -
+ podman --remote pod rm -f sharenet-production-pod 2>/dev/null || true
+ envsubst < deploy/prod-pod.yml | podman --remote kube play -
- # If --replace is NOT supported in your Podman, use this fallback instead:
- # - name: Recreate pod (fallback)
+ # If --replace IS supported in your Podman, use this instead:
+ # - name: Apply pod (kube play --replace)
# run: |
# set -euo pipefail
- # podman --remote pod rm -f sharenet-production-pod 2>/dev/null || true
- # envsubst < deploy/prod-pod.yml | podman --remote kube play -
+ # # If your Podman supports --replace, this is the cleanest:
+ # envsubst < deploy/prod-pod.yml | podman --remote kube play --replace -
# VERIFY (install curl first)
- name: Verify in-pod Nginx
diff --git a/backend/Cargo.lock b/backend/Cargo.lock
index 0f5a358..ee4af06 100644
--- a/backend/Cargo.lock
+++ b/backend/Cargo.lock
@@ -1893,6 +1893,7 @@ dependencies = [
"dotenvy",
"memory",
"tokio",
+ "utils",
]
[[package]]
@@ -1908,6 +1909,7 @@ dependencies = [
"postgres",
"sqlx",
"tokio",
+ "utils",
]
[[package]]
@@ -1921,6 +1923,7 @@ dependencies = [
"domain",
"memory",
"tokio",
+ "utils",
]
[[package]]
@@ -1936,6 +1939,7 @@ dependencies = [
"postgres",
"sqlx",
"tokio",
+ "utils",
]
[[package]]
@@ -1948,6 +1952,7 @@ dependencies = [
"memory",
"tokio",
"tui",
+ "utils",
]
[[package]]
@@ -1962,6 +1967,7 @@ dependencies = [
"sqlx",
"tokio",
"tui",
+ "utils",
]
[[package]]
@@ -2633,6 +2639,7 @@ dependencies = [
"ratatui",
"textwrap",
"tokio",
+ "uuid",
]
[[package]]
@@ -2733,6 +2740,16 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+[[package]]
+name = "utils"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "domain",
+ "dotenvy",
+ "tempfile",
+]
+
[[package]]
name = "uuid"
version = "1.17.0"
diff --git a/backend/Cargo.toml b/backend/Cargo.toml
index 5f3c057..919f8a9 100644
--- a/backend/Cargo.toml
+++ b/backend/Cargo.toml
@@ -15,7 +15,7 @@ tokio = { version = "1.36", features = ["full"] }
anyhow = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
-uuid = { version = "1.7", features = ["v4", "serde"] }
+uuid = { version = "1.8", features = ["v4", "v7", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
thiserror = "1.0"
clap = { version = "4.5", features = ["derive"] }
@@ -29,6 +29,7 @@ dotenvy = "0.15"
ratatui = "0.24"
crossterm = "0.27"
textwrap = "0.16"
+tempfile = "3.10"
[profile.release]
lto = true
diff --git a/backend/config/api-memory.env b/backend/config/api-memory.env
index cbee19e..36a77f3 100644
--- a/backend/config/api-memory.env
+++ b/backend/config/api-memory.env
@@ -2,5 +2,8 @@
HOST=127.0.0.1
PORT=3001
+# Hub Universe DID Configuration
+HUB_UNIVERSE_DID=u:Test:018f1234-5678-9abc-def0-123456789012
+
# Optional: Logging Configuration
-RUST_LOG=info
\ No newline at end of file
+RUST_LOG=info
\ No newline at end of file
diff --git a/backend/config/api-postgres.env b/backend/config/api-postgres.env
index 8e15556..62d74af 100644
--- a/backend/config/api-postgres.env
+++ b/backend/config/api-postgres.env
@@ -5,5 +5,8 @@ PORT=3001
# Database Configuration
DATABASE_URL=postgres://postgres:password@localhost:5432/sharenet
+# Hub Universe DID Configuration
+HUB_UNIVERSE_DID=u:Test:018f1234-5678-9abc-def0-123456789012
+
# Optional: Logging Configuration
-RUST_LOG=info
\ No newline at end of file
+RUST_LOG=info
\ No newline at end of file
diff --git a/backend/config/cli-memory.env b/backend/config/cli-memory.env
index d7a9434..8514a83 100644
--- a/backend/config/cli-memory.env
+++ b/backend/config/cli-memory.env
@@ -1,2 +1,5 @@
# Optional: Logging Configuration
-RUST_LOG=info
\ No newline at end of file
+RUST_LOG=info
+
+# Hub Universe DID Configuration
+HUB_UNIVERSE_DID=u:Test:018f1234-5678-9abc-def0-123456789012
\ No newline at end of file
diff --git a/backend/config/cli-postgres.env b/backend/config/cli-postgres.env
index bc69fb3..7a2135d 100644
--- a/backend/config/cli-postgres.env
+++ b/backend/config/cli-postgres.env
@@ -2,4 +2,7 @@
DATABASE_URL=postgres://postgres:password@localhost:5432/sharenet
# Optional: Logging Configuration
-RUST_LOG=info
\ No newline at end of file
+RUST_LOG=info
+
+# Hub Universe DID Configuration
+HUB_UNIVERSE_DID=u:Test:018f1234-5678-9abc-def0-123456789012
\ No newline at end of file
diff --git a/backend/config/tui-memory.env b/backend/config/tui-memory.env
index 4cfeff6..7bd32b0 100644
--- a/backend/config/tui-memory.env
+++ b/backend/config/tui-memory.env
@@ -1,2 +1,5 @@
# Memory TUI Configuration
-RUST_LOG=info
\ No newline at end of file
+RUST_LOG=info
+
+# Hub Universe DID Configuration
+HUB_UNIVERSE_DID=u:Test:018f1234-5678-9abc-def0-123456789012
\ No newline at end of file
diff --git a/backend/config/tui-postgres.env b/backend/config/tui-postgres.env
index 39319a6..e3cdf80 100644
--- a/backend/config/tui-postgres.env
+++ b/backend/config/tui-postgres.env
@@ -1,3 +1,6 @@
# Postgres TUI Configuration
RUST_LOG=info
-DATABASE_URL=postgres://postgres:password@localhost:5432/sharenet
\ No newline at end of file
+DATABASE_URL=postgres://postgres:password@localhost:5432/sharenet
+
+# Hub Universe DID Configuration
+HUB_UNIVERSE_DID=u:Test:018f1234-5678-9abc-def0-123456789012
\ No newline at end of file
diff --git a/backend/crates/api/src/lib.rs b/backend/crates/api/src/lib.rs
index 3377a7d..b8a555a 100644
--- a/backend/crates/api/src/lib.rs
+++ b/backend/crates/api/src/lib.rs
@@ -108,6 +108,7 @@ use uuid::Uuid;
pub struct AppState {
user_service: Arc,
product_service: Arc
,
+ hub_universe_did: String,
}
impl Clone for AppState
@@ -119,6 +120,7 @@ where
Self {
user_service: self.user_service.clone(),
product_service: self.product_service.clone(),
+ hub_universe_did: self.hub_universe_did.clone(),
}
}
}
@@ -133,9 +135,10 @@ where
/// * `addr` - The socket address to bind the server to
/// * `user_service` - Service implementation for user operations
/// * `product_service` - Service implementation for product operations
+/// * `hub_universe_did` - The hub universe DID for passport affiliation
///
/// See the module-level documentation for usage examples.
-pub async fn run(addr: SocketAddr, user_service: U, product_service: P)
+pub async fn run(addr: SocketAddr, user_service: U, product_service: P, hub_universe_did: String)
where
U: UseCase + Clone + Send + Sync + 'static,
P: UseCase + Clone + Send + Sync + 'static,
@@ -150,6 +153,7 @@ where
let state = AppState {
user_service: Arc::new(user_service),
product_service: Arc::new(product_service),
+ hub_universe_did,
};
// Configure CORS
@@ -160,6 +164,7 @@ where
let app = Router::new()
.route("/health", get(health_check))
+ .route("/hub/universe-did", get(get_universe_did::))
.route("/users", post(create_user::))
.route("/users/:id", get(get_user::))
.route("/users", get(list_users::))
@@ -390,6 +395,22 @@ async fn health_check() -> impl IntoResponse {
})))
}
+/// Get hub universe DID endpoint
+///
+/// Returns the hub universe DID for passport affiliation.
+/// This endpoint is used by the frontend to get the hub's universe DID
+/// when creating new passports.
+///
+/// # Response
+/// - `200 OK` - Universe DID returned successfully
+async fn get_universe_did(
+ State(state): State>,
+) -> impl IntoResponse {
+ (StatusCode::OK, Json(serde_json::json!({
+ "did": state.hub_universe_did
+ })))
+}
+
#[cfg(test)]
mod tests {
//! # API Tests
@@ -582,14 +603,16 @@ mod tests {
fn create_test_app() -> Router {
let user_service = MockUserService::new();
let product_service = MockProductService::new();
-
+
let state = AppState {
user_service: Arc::new(user_service),
product_service: Arc::new(product_service),
+ hub_universe_did: "u:hub:12345678-1234-1234-1234-123456789012".to_string(),
};
Router::new()
.route("/health", get(health_check))
+ .route("/hub/universe-did", get(get_universe_did::))
.route("/users", post(create_user::))
.route("/users/:id", get(get_user::))
.route("/users", get(list_users::))
@@ -995,7 +1018,7 @@ mod tests {
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
-
+
let health_data: serde_json::Value = extract_json(response).await;
assert_eq!(health_data["status"], "healthy");
assert_eq!(health_data["service"], "sharenet-api");
@@ -1003,6 +1026,61 @@ mod tests {
}
}
+ mod universe_did_endpoint {
+ //! # Universe DID Endpoint Tests
+ //!
+ //! Tests for the hub universe DID endpoint used by the frontend
+ //! to get the hub's universe DID for passport affiliation.
+
+ use super::*;
+
+ /// Tests the universe DID endpoint returns the configured DID.
+ #[tokio::test]
+ async fn test_get_universe_did() {
+ let app = create_test_app();
+
+ let response = app
+ .oneshot(
+ Request::builder()
+ .method("GET")
+ .uri("/hub/universe-did")
+ .body(Body::empty())
+ .unwrap(),
+ )
+ .await
+ .unwrap();
+
+ assert_eq!(response.status(), StatusCode::OK);
+
+ let did_data: serde_json::Value = extract_json(response).await;
+ assert_eq!(did_data["did"], "u:hub:12345678-1234-1234-1234-123456789012");
+ }
+
+ /// Tests that the universe DID endpoint returns a valid JSON structure.
+ #[tokio::test]
+ async fn test_universe_did_response_structure() {
+ let app = create_test_app();
+
+ let response = app
+ .oneshot(
+ Request::builder()
+ .method("GET")
+ .uri("/hub/universe-did")
+ .body(Body::empty())
+ .unwrap(),
+ )
+ .await
+ .unwrap();
+
+ assert_eq!(response.status(), StatusCode::OK);
+
+ let did_data: serde_json::Value = extract_json(response).await;
+ assert!(did_data.is_object());
+ assert!(did_data.get("did").is_some());
+ assert!(did_data["did"].is_string());
+ }
+ }
+
mod product_endpoints {
//! # Product Endpoint Tests
//!
diff --git a/backend/crates/cli/src/lib.rs b/backend/crates/cli/src/lib.rs
index daa1bd2..623fd12 100644
--- a/backend/crates/cli/src/lib.rs
+++ b/backend/crates/cli/src/lib.rs
@@ -11,7 +11,7 @@
use anyhow::Result;
use clap::Parser;
-use domain::{CreateProduct, CreateUser, Product, User, UpdateProduct, UpdateUser};
+use domain::{CreateProduct, CreateUser, Product, User, UpdateProduct, UpdateUser, UniverseDid};
use application::UseCase;
use uuid::Uuid;
@@ -34,6 +34,11 @@ pub enum Commands {
#[command(subcommand)]
command: ProductCommands,
},
+ /// Universe DID management commands
+ UniverseDid {
+ #[command(subcommand)]
+ command: UniverseDidCommands,
+ },
}
#[derive(Parser)]
@@ -114,8 +119,28 @@ pub enum ProductCommands {
},
}
+#[derive(Parser)]
+pub enum UniverseDidCommands {
+ /// Create a new universe DID
+ Create {
+ /// Universe name
+ #[arg(short, long)]
+ name: String,
+ },
+ /// Validate a universe DID
+ Validate {
+ /// Universe DID to validate
+ #[arg(short, long)]
+ did: String,
+ },
+ /// Generate a default hub universe DID
+ GenerateHub,
+ /// Get the hub universe DID
+ GetHub,
+}
+
impl Cli {
- pub async fn run(self, user_service: U, product_service: P) -> Result<()>
+ pub async fn run(self, user_service: U, product_service: P, hub_universe_did: String) -> Result<()>
where
U: UseCase,
P: UseCase,
@@ -167,6 +192,54 @@ impl Cli {
println!("Deleted product {}", id);
}
},
+ Some(Commands::UniverseDid { command }) => match command {
+ UniverseDidCommands::Create { name } => {
+ let universe_did = UniverseDid::new(format!("u:{}:{}", name, uuid::Uuid::now_v7()))?;
+ println!("Created universe DID: {}", universe_did.did());
+ }
+ UniverseDidCommands::Validate { did } => {
+ match UniverseDid::new(did.clone()) {
+ Ok(universe_did) => {
+ println!("Valid universe DID: {}", universe_did.did());
+ if let Some(name) = universe_did.name() {
+ println!(" Name: {}", name);
+ }
+ if let Some(uuid) = universe_did.uuid() {
+ println!(" UUID: {}", uuid);
+ }
+ }
+ Err(e) => {
+ println!("Invalid universe DID '{}': {}", did, e);
+ }
+ }
+ }
+ UniverseDidCommands::GenerateHub => {
+ let hub_did = UniverseDid::default_hub();
+ println!("Generated hub universe DID: {}", hub_did.did());
+ if let Some(name) = hub_did.name() {
+ println!(" Name: {}", name);
+ }
+ if let Some(uuid) = hub_did.uuid() {
+ println!(" UUID: {}", uuid);
+ }
+ }
+ UniverseDidCommands::GetHub => {
+ println!("Hub universe DID: {}", hub_universe_did);
+ match UniverseDid::new(hub_universe_did.clone()) {
+ Ok(universe_did) => {
+ if let Some(name) = universe_did.name() {
+ println!(" Name: {}", name);
+ }
+ if let Some(uuid) = universe_did.uuid() {
+ println!(" UUID: {}", uuid);
+ }
+ }
+ Err(e) => {
+ println!("Warning: Hub universe DID is invalid: {}", e);
+ }
+ }
+ }
+ },
None => {
println!("No command provided. Use --help for usage information.");
}
diff --git a/backend/crates/domain/src/lib.rs b/backend/crates/domain/src/lib.rs
index df889e5..d00c776 100644
--- a/backend/crates/domain/src/lib.rs
+++ b/backend/crates/domain/src/lib.rs
@@ -838,3 +838,181 @@ mod tests {
}
}
}
+
+// Hub Universe DID domain model
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct UniverseDid {
+ did: String,
+}
+
+impl UniverseDid {
+ pub fn new(did: String) -> Result {
+ if did.trim().is_empty() {
+ return Err(DomainError::InvalidInput("DID cannot be empty".to_string()));
+ }
+
+ // Universe DID format validation: u:name:uuidv7
+ let parts: Vec<&str> = did.split(':').collect();
+ if parts.len() != 3 || parts[0] != "u" {
+ return Err(DomainError::InvalidInput(
+ "Universe DID must follow format: u:name:uuidv7".to_string()
+ ));
+ }
+
+ // Validate UUIDv7 format
+ if uuid::Uuid::parse_str(parts[2]).is_err() {
+ return Err(DomainError::InvalidInput(
+ "Invalid UUIDv7 format in Universe DID".to_string()
+ ));
+ }
+
+ Ok(Self { did })
+ }
+
+ // Constructor for generating a default hub universe DID
+ pub fn default_hub() -> Self {
+ Self {
+ did: format!("u:hub:{}", uuid::Uuid::now_v7()),
+ }
+ }
+
+ // Getters
+ pub fn did(&self) -> &str {
+ &self.did
+ }
+
+ // Get the name part of the universe DID
+ pub fn name(&self) -> Option<&str> {
+ let parts: Vec<&str> = self.did.split(':').collect();
+ if parts.len() == 3 {
+ Some(parts[1])
+ } else {
+ None
+ }
+ }
+
+ // Get the UUID part of the universe DID
+ pub fn uuid(&self) -> Option {
+ let parts: Vec<&str> = self.did.split(':').collect();
+ if parts.len() == 3 {
+ uuid::Uuid::parse_str(parts[2]).ok()
+ } else {
+ None
+ }
+ }
+}
+
+impl Entity for UniverseDid {
+ type Create = ();
+ type Update = ();
+}
+
+#[cfg(test)]
+mod universe_did_tests {
+ use super::*;
+
+ #[test]
+ fn test_universe_did_valid_format() {
+ let valid_did = format!("u:test:{}", uuid::Uuid::now_v7());
+ let universe_did = UniverseDid::new(valid_did.clone()).unwrap();
+
+ assert_eq!(universe_did.did(), valid_did);
+ assert_eq!(universe_did.name(), Some("test"));
+ assert!(universe_did.uuid().is_some());
+ }
+
+ #[test]
+ fn test_universe_did_invalid_format() {
+ // Missing 'u' prefix
+ let result = UniverseDid::new("did:test:12345678-1234-1234-1234-123456789012".to_string());
+ assert!(result.is_err());
+ assert!(matches!(result.unwrap_err(), DomainError::InvalidInput(_)));
+
+ // Wrong number of parts
+ let result = UniverseDid::new("u:test".to_string());
+ assert!(result.is_err());
+ assert!(matches!(result.unwrap_err(), DomainError::InvalidInput(_)));
+
+ // Invalid UUID
+ let result = UniverseDid::new("u:test:invalid-uuid".to_string());
+ assert!(result.is_err());
+ assert!(matches!(result.unwrap_err(), DomainError::InvalidInput(_)));
+
+ // Empty DID
+ let result = UniverseDid::new("".to_string());
+ assert!(result.is_err());
+ assert!(matches!(result.unwrap_err(), DomainError::InvalidInput(_)));
+ }
+
+ #[test]
+ fn test_universe_did_default_hub() {
+ let hub_did = UniverseDid::default_hub();
+
+ assert!(hub_did.did().starts_with("u:hub:"));
+ assert_eq!(hub_did.name(), Some("hub"));
+ assert!(hub_did.uuid().is_some());
+ }
+
+ #[test]
+ fn test_universe_did_name_extraction() {
+ let universe_did = UniverseDid::new("u:myuniverse:12345678-1234-1234-1234-123456789012".to_string()).unwrap();
+
+ assert_eq!(universe_did.name(), Some("myuniverse"));
+ }
+
+ #[test]
+ fn test_universe_did_uuid_extraction() {
+ let uuid = uuid::Uuid::now_v7();
+ let universe_did = UniverseDid::new(format!("u:test:{}", uuid)).unwrap();
+
+ assert_eq!(universe_did.uuid(), Some(uuid));
+ }
+
+ #[test]
+ fn test_universe_did_validation_edge_cases() {
+ // Test empty string
+ let result = UniverseDid::new("".to_string());
+ assert!(result.is_err());
+
+ // Test whitespace only
+ let result = UniverseDid::new(" ".to_string());
+ assert!(result.is_err());
+
+ // Test missing 'u' prefix
+ let result = UniverseDid::new("did:test:12345678-1234-1234-1234-123456789012".to_string());
+ assert!(result.is_err());
+
+ // Test wrong number of parts (too few)
+ let result = UniverseDid::new("u:test".to_string());
+ assert!(result.is_err());
+
+ // Test wrong number of parts (too many)
+ let result = UniverseDid::new("u:test:uuid:extra".to_string());
+ assert!(result.is_err());
+
+ // Test invalid UUID format
+ let result = UniverseDid::new("u:test:not-a-uuid".to_string());
+ assert!(result.is_err());
+
+ // Test valid format with different names
+ let valid_did = format!("u:myuniverse:{}", uuid::Uuid::now_v7());
+ let result = UniverseDid::new(valid_did.clone());
+ assert!(result.is_ok());
+ assert_eq!(result.unwrap().did(), valid_did);
+ }
+
+ #[test]
+ fn test_universe_did_name_and_uuid_extraction_edge_cases() {
+ // Test with complex name
+ let universe_did = UniverseDid::new("u:my-complex-universe-name:12345678-1234-1234-1234-123456789012".to_string()).unwrap();
+ assert_eq!(universe_did.name(), Some("my-complex-universe-name"));
+
+ // Test with numbers in name
+ let universe_did = UniverseDid::new("u:universe123:12345678-1234-1234-1234-123456789012".to_string()).unwrap();
+ assert_eq!(universe_did.name(), Some("universe123"));
+
+ // Test with underscores in name
+ let universe_did = UniverseDid::new("u:my_universe:12345678-1234-1234-1234-123456789012".to_string()).unwrap();
+ assert_eq!(universe_did.name(), Some("my_universe"));
+ }
+}
diff --git a/backend/crates/sharenet-api-memory/Cargo.toml b/backend/crates/sharenet-api-memory/Cargo.toml
index 04b35ed..3addb1c 100644
--- a/backend/crates/sharenet-api-memory/Cargo.toml
+++ b/backend/crates/sharenet-api-memory/Cargo.toml
@@ -15,3 +15,4 @@ clap = { workspace = true, features = ["derive"] }
tokio = { workspace = true, features = ["full", "macros", "rt-multi-thread"] }
anyhow = { workspace = true }
dotenvy = { workspace = true }
+utils = { path = "../utils" }
diff --git a/backend/crates/sharenet-api-memory/src/main.rs b/backend/crates/sharenet-api-memory/src/main.rs
index f29056a..2fe8ae0 100644
--- a/backend/crates/sharenet-api-memory/src/main.rs
+++ b/backend/crates/sharenet-api-memory/src/main.rs
@@ -6,11 +6,12 @@ use application::Service;
use domain::{User, Product};
use memory::{InMemoryProductRepository, InMemoryUserRepository};
use std::env;
+use utils::HubConfig;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
- // Load environment variables from config file
- dotenvy::from_path("config/api-memory.env").ok();
+ // Load and validate hub universe DID from environment file
+ let hub_universe_did = HubConfig::read_default_hub_universe_did_from_file("config/api-memory.env")?;
// Get configuration from environment variables
let host = env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
@@ -27,7 +28,7 @@ async fn main() -> anyhow::Result<()> {
// Run API server
let addr = format!("{}:{}", host, port);
let addr = SocketAddr::from_str(&addr)?;
- run_api(addr, user_service, product_service).await;
+ run_api(addr, user_service, product_service, hub_universe_did.did().to_string()).await;
Ok(())
}
\ No newline at end of file
diff --git a/backend/crates/sharenet-api-postgres/Cargo.toml b/backend/crates/sharenet-api-postgres/Cargo.toml
index 9b81847..8b22813 100644
--- a/backend/crates/sharenet-api-postgres/Cargo.toml
+++ b/backend/crates/sharenet-api-postgres/Cargo.toml
@@ -14,4 +14,5 @@ clap = { workspace = true, features = ["derive"] }
tokio = { workspace = true, features = ["full", "macros", "rt-multi-thread"] }
anyhow = { workspace = true }
sqlx = { workspace = true }
-dotenvy = { workspace = true }
\ No newline at end of file
+dotenvy = { workspace = true }
+utils = { path = "../utils" }
\ No newline at end of file
diff --git a/backend/crates/sharenet-api-postgres/src/main.rs b/backend/crates/sharenet-api-postgres/src/main.rs
index f6adb92..8ecd48d 100644
--- a/backend/crates/sharenet-api-postgres/src/main.rs
+++ b/backend/crates/sharenet-api-postgres/src/main.rs
@@ -6,13 +6,13 @@ use application::Service;
use domain::{User, Product};
use postgres::{PostgresProductRepository, PostgresUserRepository};
use sqlx::postgres::PgPoolOptions;
-use dotenvy;
use std::env;
+use utils::HubConfig;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
- // Load environment variables from config file
- dotenvy::from_path("config/api-postgres.env").ok();
+ // Load and validate hub universe DID from environment file
+ let hub_universe_did = HubConfig::read_default_hub_universe_did_from_file("config/api-postgres.env")?;
// Get configuration from environment variables
let host = env::var("HOST").unwrap_or_else(|_| "0.0.0.0".to_string());
@@ -41,7 +41,7 @@ async fn main() -> anyhow::Result<()> {
// Run API server
let addr = format!("{}:{}", host, port);
let addr = SocketAddr::from_str(&addr)?;
- run_api(addr, user_service, product_service).await;
+ run_api(addr, user_service, product_service, hub_universe_did.did().to_string()).await;
Ok(())
}
\ No newline at end of file
diff --git a/backend/crates/sharenet-cli-memory/Cargo.toml b/backend/crates/sharenet-cli-memory/Cargo.toml
index a6eb509..a4bd01b 100644
--- a/backend/crates/sharenet-cli-memory/Cargo.toml
+++ b/backend/crates/sharenet-cli-memory/Cargo.toml
@@ -12,4 +12,5 @@ application = { path = "../application" }
cli = { path = "../cli" }
memory = { path = "../memory" }
tokio = { workspace = true }
-clap = { workspace = true }
\ No newline at end of file
+clap = { workspace = true }
+utils = { path = "../utils" }
\ No newline at end of file
diff --git a/backend/crates/sharenet-cli-memory/src/main.rs b/backend/crates/sharenet-cli-memory/src/main.rs
index e8818ff..b539329 100644
--- a/backend/crates/sharenet-cli-memory/src/main.rs
+++ b/backend/crates/sharenet-cli-memory/src/main.rs
@@ -2,12 +2,16 @@ use anyhow::Result;
use clap::Parser;
use memory::{MemoryUserService, MemoryProductService};
use cli::Cli;
+use utils::HubConfig;
#[tokio::main]
async fn main() -> Result<()> {
+ // Load and validate hub universe DID from environment file
+ let hub_universe_did = HubConfig::read_default_hub_universe_did_from_file("config/cli-memory.env")?;
+
let user_service = MemoryUserService::new(memory::InMemoryUserRepository::new());
let product_service = MemoryProductService::new(memory::InMemoryProductRepository::new());
-
+
let cli = Cli::try_parse()?;
- cli.run(user_service, product_service).await
+ cli.run(user_service, product_service, hub_universe_did.did().to_string()).await
}
\ No newline at end of file
diff --git a/backend/crates/sharenet-cli-postgres/Cargo.toml b/backend/crates/sharenet-cli-postgres/Cargo.toml
index 4d6875a..8182ea0 100644
--- a/backend/crates/sharenet-cli-postgres/Cargo.toml
+++ b/backend/crates/sharenet-cli-postgres/Cargo.toml
@@ -14,4 +14,5 @@ postgres = { path = "../postgres" }
tokio = { workspace = true }
clap = { workspace = true }
dotenvy = { workspace = true }
-sqlx = { workspace = true }
\ No newline at end of file
+sqlx = { workspace = true }
+utils = { path = "../utils" }
\ No newline at end of file
diff --git a/backend/crates/sharenet-cli-postgres/src/main.rs b/backend/crates/sharenet-cli-postgres/src/main.rs
index 3e275cf..c536ac1 100644
--- a/backend/crates/sharenet-cli-postgres/src/main.rs
+++ b/backend/crates/sharenet-cli-postgres/src/main.rs
@@ -2,14 +2,19 @@ use anyhow::Result;
use clap::Parser;
use postgres::{PostgresUserService, PostgresProductService};
use cli::Cli;
+use utils::HubConfig;
#[tokio::main]
async fn main() -> Result<()> {
dotenvy::from_path("config/cli-postgres.env").ok();
+
+ // Load and validate hub universe DID from environment file
+ let hub_universe_did = HubConfig::read_default_hub_universe_did_from_file("config/cli-postgres.env")?;
+
let pool = sqlx::PgPool::connect(&std::env::var("DATABASE_URL")?).await?;
let user_service = PostgresUserService::new(postgres::PostgresUserRepository::new(pool.clone()));
let product_service = PostgresProductService::new(postgres::PostgresProductRepository::new(pool));
-
+
let cli = Cli::try_parse()?;
- cli.run(user_service, product_service).await
+ cli.run(user_service, product_service, hub_universe_did.did().to_string()).await
}
\ No newline at end of file
diff --git a/backend/crates/sharenet-tui-memory/Cargo.toml b/backend/crates/sharenet-tui-memory/Cargo.toml
index a056d55..1ad9673 100644
--- a/backend/crates/sharenet-tui-memory/Cargo.toml
+++ b/backend/crates/sharenet-tui-memory/Cargo.toml
@@ -12,3 +12,4 @@ application = { path = "../application" }
tui = { path = "../tui" }
memory = { path = "../memory" }
tokio = { workspace = true }
+utils = { path = "../utils" }
diff --git a/backend/crates/sharenet-tui-memory/src/main.rs b/backend/crates/sharenet-tui-memory/src/main.rs
index 58fb7af..fa92609 100644
--- a/backend/crates/sharenet-tui-memory/src/main.rs
+++ b/backend/crates/sharenet-tui-memory/src/main.rs
@@ -1,11 +1,15 @@
use anyhow::Result;
use memory::{MemoryUserService, MemoryProductService};
use tui::run_tui;
+use utils::HubConfig;
#[tokio::main]
async fn main() -> Result<()> {
+ // Load and validate hub universe DID from environment file
+ let hub_universe_did = HubConfig::read_default_hub_universe_did_from_file("config/tui-memory.env")?;
+
let user_service = MemoryUserService::new(memory::InMemoryUserRepository::new());
let product_service = MemoryProductService::new(memory::InMemoryProductRepository::new());
-
- run_tui(user_service, product_service).await
+
+ run_tui(user_service, product_service, hub_universe_did.did().to_string()).await
}
diff --git a/backend/crates/sharenet-tui-postgres/Cargo.toml b/backend/crates/sharenet-tui-postgres/Cargo.toml
index 104bbc6..e00b3ea 100644
--- a/backend/crates/sharenet-tui-postgres/Cargo.toml
+++ b/backend/crates/sharenet-tui-postgres/Cargo.toml
@@ -14,3 +14,4 @@ postgres = { path = "../postgres" }
tokio = { workspace = true }
dotenvy = { workspace = true }
sqlx = { workspace = true }
+utils = { path = "../utils" }
diff --git a/backend/crates/sharenet-tui-postgres/src/main.rs b/backend/crates/sharenet-tui-postgres/src/main.rs
index 4744820..ccbc4d6 100644
--- a/backend/crates/sharenet-tui-postgres/src/main.rs
+++ b/backend/crates/sharenet-tui-postgres/src/main.rs
@@ -1,6 +1,7 @@
use anyhow::Result;
use postgres::{PostgresUserService, PostgresProductService};
use tui::run_tui;
+use utils::HubConfig;
#[tokio::main]
async fn main() -> Result<()> {
@@ -36,8 +37,11 @@ async fn main() -> Result<()> {
Err(e) => println!("DATABASE_URL not found: {}", e),
}
+ // Load and validate hub universe DID from environment file
+ let hub_universe_did = HubConfig::read_default_hub_universe_did_from_file("config/tui-postgres.env")?;
+
let pool = sqlx::PgPool::connect(&std::env::var("DATABASE_URL")?).await?;
let user_service = PostgresUserService::new(postgres::PostgresUserRepository::new(pool.clone()));
let product_service = PostgresProductService::new(postgres::PostgresProductRepository::new(pool));
- run_tui(user_service, product_service).await
+ run_tui(user_service, product_service, hub_universe_did.did().to_string()).await
}
diff --git a/backend/crates/tui/Cargo.toml b/backend/crates/tui/Cargo.toml
index acded88..e3621d7 100644
--- a/backend/crates/tui/Cargo.toml
+++ b/backend/crates/tui/Cargo.toml
@@ -13,4 +13,5 @@ memory = { path = "../memory" }
ratatui = { workspace = true }
crossterm = { workspace = true }
textwrap = "0.16"
-tokio = { workspace = true }
\ No newline at end of file
+tokio = { workspace = true }
+uuid = { workspace = true }
\ No newline at end of file
diff --git a/backend/crates/tui/src/lib.rs b/backend/crates/tui/src/lib.rs
index 9a3b2db..1bad6c1 100644
--- a/backend/crates/tui/src/lib.rs
+++ b/backend/crates/tui/src/lib.rs
@@ -21,7 +21,7 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
-use domain::{CreateProduct, CreateUser, Product, User};
+use domain::{CreateProduct, CreateUser, Product, User, UniverseDid};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
@@ -142,7 +142,7 @@ impl App {
}
}
-pub async fn run_tui(user_service: U, product_service: P) -> anyhow::Result<()>
+pub async fn run_tui(user_service: U, product_service: P, hub_universe_did: String) -> anyhow::Result<()>
where
U: UseCase + Clone + Send + 'static,
P: UseCase + Clone + Send + 'static,
@@ -156,7 +156,7 @@ where
// Create app and run it
let app = App::new();
- let res = run_app(&mut terminal, app, user_service, product_service).await;
+ let res = run_app(&mut terminal, app, user_service, product_service, hub_universe_did).await;
// Restore terminal
disable_raw_mode()?;
@@ -179,6 +179,7 @@ async fn run_app(
mut app: App,
user_service: U,
product_service: P,
+ hub_universe_did: String,
) -> anyhow::Result<()>
where
U: UseCase + Clone + Send + 'static,
@@ -307,6 +308,62 @@ where
Err(e) => app.add_message(format!("Error: {}", e)),
}
}
+ cmd if cmd.starts_with("universe-did create") => {
+ match parse_universe_did_create(cmd) {
+ Ok(name) => {
+ match UniverseDid::new(format!("u:{}:{}", name, uuid::Uuid::now_v7())) {
+ Ok(universe_did) => app.add_message(format!("Created universe DID: {}", universe_did.did())),
+ Err(e) => app.add_message(format!("Error: {}", e)),
+ }
+ }
+ Err(e) => app.add_message(format!("Error: {}", e)),
+ }
+ }
+ cmd if cmd.starts_with("universe-did validate") => {
+ match parse_universe_did_validate(cmd) {
+ Ok(did) => {
+ match UniverseDid::new(did.clone()) {
+ Ok(universe_did) => {
+ let mut message = format!("Valid universe DID: {}", universe_did.did());
+ if let Some(name) = universe_did.name() {
+ message.push_str(&format!("\n Name: {}", name));
+ }
+ if let Some(uuid) = universe_did.uuid() {
+ message.push_str(&format!("\n UUID: {}", uuid));
+ }
+ app.add_message(message);
+ }
+ Err(e) => app.add_message(format!("Invalid universe DID '{}': {}", did, e)),
+ }
+ }
+ Err(e) => app.add_message(format!("Error: {}", e)),
+ }
+ }
+ "universe-did generate-hub" => {
+ let hub_did = UniverseDid::default_hub();
+ let mut message = format!("Generated hub universe DID: {}", hub_did.did());
+ if let Some(name) = hub_did.name() {
+ message.push_str(&format!("\n Name: {}", name));
+ }
+ if let Some(uuid) = hub_did.uuid() {
+ message.push_str(&format!("\n UUID: {}", uuid));
+ }
+ app.add_message(message);
+ }
+ "universe-did get-hub" => {
+ app.add_message(format!("Hub universe DID: {}", hub_universe_did));
+ match UniverseDid::new(hub_universe_did.clone()) {
+ Ok(universe_did) => {
+ if let Some(name) = universe_did.name() {
+ app.add_message(format!(" Name: {}", name));
+ }
+ if let Some(uuid) = universe_did.uuid() {
+ app.add_message(format!(" UUID: {}", uuid));
+ }
+ }
+ Err(e) => app.add_message(format!("Warning: Hub universe DID is invalid: {}", e)),
+ }
+ }
"" => {}
_ => {
app.add_message("Unknown command. Type 'help' for available commands.".to_string());
@@ -334,6 +391,12 @@ fn print_help(app: &mut App) {
app.add_message(" product create -n -d ".to_string());
app.add_message(" Example: product create -n \"My Product\" -d \"A great product description\"".to_string());
app.add_message(" product list".to_string());
+ app.add_message(" universe-did create -n ".to_string());
+ app.add_message(" Example: universe-did create -n myuniverse".to_string());
+ app.add_message(" universe-did validate -d ".to_string());
+ app.add_message(" Example: universe-did validate -d u:myuniverse:12345678-1234-1234-1234-123456789012".to_string());
+ app.add_message(" universe-did generate-hub".to_string());
+ app.add_message(" universe-did get-hub".to_string());
app.add_message("\nTips:".to_string());
app.add_message(" - Use quotes for values with spaces".to_string());
app.add_message(" - Use Up/Down arrows to navigate command history".to_string());
@@ -532,6 +595,110 @@ fn parse_product_create(cmd: &str) -> anyhow::Result<(String, String)> {
}
}
+fn parse_universe_did_create(cmd: &str) -> anyhow::Result {
+ let parts: Vec<&str> = cmd.split_whitespace().collect();
+ if parts.len() < 4 {
+ return Err(anyhow::anyhow!(
+ "Invalid command format. Use: universe-did create -n \nExample: universe-did create -n myuniverse"
+ ));
+ }
+
+ let mut name = None;
+ let mut current_arg = None;
+ let mut current_value = Vec::new();
+
+ // Skip "universe-did create" command
+ let mut i = 2;
+ while i < parts.len() {
+ match parts[i] {
+ "-n" => {
+ if let Some(arg_type) = current_arg {
+ match arg_type {
+ "name" => name = Some(current_value.join(" ")),
+ _ => {}
+ }
+ }
+ current_arg = Some("name");
+ current_value.clear();
+ i += 1;
+ }
+ _ => {
+ if current_arg.is_some() {
+ current_value.push(parts[i].trim_matches('"'));
+ }
+ i += 1;
+ }
+ }
+ }
+
+ // Handle the last argument
+ if let Some(arg_type) = current_arg {
+ match arg_type {
+ "name" => name = Some(current_value.join(" ")),
+ _ => {}
+ }
+ }
+
+ match name {
+ Some(n) if !n.is_empty() => Ok(n),
+ _ => Err(anyhow::anyhow!(
+ "Invalid command format. Use: universe-did create -n \nExample: universe-did create -n myuniverse"
+ )),
+ }
+}
+
+fn parse_universe_did_validate(cmd: &str) -> anyhow::Result {
+ let parts: Vec<&str> = cmd.split_whitespace().collect();
+ if parts.len() < 4 {
+ return Err(anyhow::anyhow!(
+ "Invalid command format. Use: universe-did validate -d \nExample: universe-did validate -d u:myuniverse:12345678-1234-1234-1234-123456789012"
+ ));
+ }
+
+ let mut did = None;
+ let mut current_arg = None;
+ let mut current_value = Vec::new();
+
+ // Skip "universe-did validate" command
+ let mut i = 2;
+ while i < parts.len() {
+ match parts[i] {
+ "-d" => {
+ if let Some(arg_type) = current_arg {
+ match arg_type {
+ "did" => did = Some(current_value.join(" ")),
+ _ => {}
+ }
+ }
+ current_arg = Some("did");
+ current_value.clear();
+ i += 1;
+ }
+ _ => {
+ if current_arg.is_some() {
+ current_value.push(parts[i].trim_matches('"'));
+ }
+ i += 1;
+ }
+ }
+ }
+
+ // Handle the last argument
+ if let Some(arg_type) = current_arg {
+ match arg_type {
+ "did" => did = Some(current_value.join(" ")),
+ _ => {}
+ }
+ }
+
+ match did {
+ Some(d) if !d.is_empty() => Ok(d),
+ _ => Err(anyhow::anyhow!(
+ "Invalid command format. Use: universe-did validate -d \nExample: universe-did validate -d u:myuniverse:12345678-1234-1234-1234-123456789012"
+ )),
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/backend/crates/utils/Cargo.toml b/backend/crates/utils/Cargo.toml
new file mode 100644
index 0000000..924b307
--- /dev/null
+++ b/backend/crates/utils/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "utils"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+license.workspace = true
+
+[dependencies]
+anyhow.workspace = true
+domain = { path = "../domain" }
+dotenvy.workspace = true
+
+[dev-dependencies]
+tempfile.workspace = true
\ No newline at end of file
diff --git a/backend/crates/utils/src/lib.rs b/backend/crates/utils/src/lib.rs
new file mode 100644
index 0000000..8ae4902
--- /dev/null
+++ b/backend/crates/utils/src/lib.rs
@@ -0,0 +1,129 @@
+/*
+ * This file is part of Sharenet.
+ *
+ * Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
+ *
+ * You may obtain a copy of the license at:
+ * https://creativecommons.org/licenses/by-nc-sa/4.0/
+ *
+ * Copyright (c) 2024 Continuist
+ */
+
+use anyhow::Result;
+use domain::UniverseDid;
+use std::env;
+
+/// Configuration utility for reading and validating hub universe DID from environment
+pub struct HubConfig;
+
+impl HubConfig {
+ /// Read and validate hub universe DID from environment variable
+ ///
+ /// # Arguments
+ /// * `env_var` - The environment variable name to read from
+ ///
+ /// # Returns
+ /// * `Result` - Validated universe DID or error
+ pub fn read_hub_universe_did(env_var: &str) -> Result {
+ let hub_universe_did = env::var(env_var)
+ .map_err(|_| anyhow::anyhow!("{} must be set", env_var))?;
+
+ let universe_did = UniverseDid::new(hub_universe_did.clone())
+ .map_err(|e| anyhow::anyhow!("Invalid {} format: {}", env_var, e))?;
+
+ Ok(universe_did)
+ }
+
+ /// Read hub universe DID with default environment variable name "HUB_UNIVERSE_DID"
+ pub fn read_default_hub_universe_did() -> Result {
+ Self::read_hub_universe_did("HUB_UNIVERSE_DID")
+ }
+
+ /// Read hub universe DID from a specific environment file
+ ///
+ /// # Arguments
+ /// * `env_file_path` - Path to the environment file
+ /// * `env_var` - The environment variable name to read
+ ///
+ /// # Returns
+ /// * `Result` - Validated universe DID or error
+ pub fn read_hub_universe_did_from_file(env_file_path: &str, env_var: &str) -> Result {
+ // Load environment variables from the specified file
+ dotenvy::from_path(env_file_path)
+ .map_err(|e| anyhow::anyhow!("Failed to load environment file {}: {}", env_file_path, e))?;
+
+ Self::read_hub_universe_did(env_var)
+ }
+
+ /// Read hub universe DID from default environment file with default variable name
+ pub fn read_default_hub_universe_did_from_file(env_file_path: &str) -> Result {
+ Self::read_hub_universe_did_from_file(env_file_path, "HUB_UNIVERSE_DID")
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::env;
+ use tempfile::NamedTempFile;
+ use std::io::Write;
+
+ #[test]
+ fn test_read_hub_universe_did_valid() {
+ let valid_did = "u:hub:12345678-1234-1234-1234-123456789012";
+ env::set_var("TEST_HUB_DID", valid_did);
+
+ let result = HubConfig::read_hub_universe_did("TEST_HUB_DID");
+ assert!(result.is_ok());
+ assert_eq!(result.unwrap().did(), valid_did);
+
+ env::remove_var("TEST_HUB_DID");
+ }
+
+ #[test]
+ fn test_read_hub_universe_did_missing() {
+ env::remove_var("TEST_MISSING_DID");
+
+ let result = HubConfig::read_hub_universe_did("TEST_MISSING_DID");
+ assert!(result.is_err());
+ assert!(result.unwrap_err().to_string().contains("must be set"));
+ }
+
+ #[test]
+ fn test_read_hub_universe_did_invalid_format() {
+ env::set_var("TEST_INVALID_DID", "invalid-format");
+
+ let result = HubConfig::read_hub_universe_did("TEST_INVALID_DID");
+ assert!(result.is_err());
+ assert!(result.unwrap_err().to_string().contains("Invalid format"));
+
+ env::remove_var("TEST_INVALID_DID");
+ }
+
+ #[test]
+ fn test_read_from_file_valid() {
+ let mut temp_file = NamedTempFile::new().unwrap();
+ writeln!(temp_file, "HUB_UNIVERSE_DID=u:hub:12345678-1234-1234-1234-123456789012").unwrap();
+
+ let result = HubConfig::read_hub_universe_did_from_file(temp_file.path().to_str().unwrap(), "HUB_UNIVERSE_DID");
+ assert!(result.is_ok());
+ assert_eq!(result.unwrap().did(), "u:hub:12345678-1234-1234-1234-123456789012");
+ }
+
+ #[test]
+ fn test_read_from_file_missing_file() {
+ let result = HubConfig::read_hub_universe_did_from_file("/nonexistent/file.env", "HUB_UNIVERSE_DID");
+ assert!(result.is_err());
+ assert!(result.unwrap_err().to_string().contains("Failed to load environment file"));
+ }
+
+ #[test]
+ fn test_read_from_file_missing_var() {
+ let mut temp_file = NamedTempFile::new().unwrap();
+ writeln!(temp_file, "OTHER_VAR=value").unwrap();
+
+ let result = HubConfig::read_hub_universe_did_from_file(temp_file.path().to_str().unwrap(), "HUB_UNIVERSE_DID");
+ assert!(result.is_err());
+ assert!(result.unwrap_err().to_string().contains("must be set"));
+ }
+}
\ No newline at end of file
diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json
new file mode 100644
index 0000000..c570e9b
--- /dev/null
+++ b/frontend/.eslintrc.json
@@ -0,0 +1,4 @@
+{
+ "extends": "next/core-web-vitals",
+ "ignorePatterns": ["src/lib/wasm-pkg/**"]
+}
\ No newline at end of file
diff --git a/frontend/Dockerfile b/frontend/Dockerfile
index 72894ea..f18b1b1 100644
--- a/frontend/Dockerfile
+++ b/frontend/Dockerfile
@@ -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 ----------
FROM docker.io/node:20-slim AS builder
WORKDIR /app
@@ -6,7 +33,8 @@ WORKDIR /app
COPY package*.json ./
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 . .
# 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/static ./.next/static
+# Copy WASM files to standalone output
+COPY --from=builder /app/src/lib/wasm-pkg ./src/lib/wasm-pkg/
+
# non-root (optional)
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 nextjs \
diff --git a/frontend/debug_spf.html b/frontend/debug_spf.html
new file mode 100644
index 0000000..071cf28
--- /dev/null
+++ b/frontend/debug_spf.html
@@ -0,0 +1,46 @@
+
+
+
+ SPF File Debug
+
+
+ SPF File Debug
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/next.config.ts b/frontend/next.config.ts
index cdafba7..fc41b9d 100644
--- a/frontend/next.config.ts
+++ b/frontend/next.config.ts
@@ -22,6 +22,32 @@ const nextConfig: NextConfig = {
// Webpack optimizations
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
if (!dev && !isServer) {
config.optimization = {
@@ -38,7 +64,7 @@ const nextConfig: NextConfig = {
},
};
}
-
+
return config;
},
@@ -60,6 +86,12 @@ const nextConfig: NextConfig = {
compiler: {
removeConsole: process.env.NODE_ENV === 'production',
},
+
+ // ESLint configuration
+ eslint: {
+ // Don't run ESLint during build for WASM files
+ ignoreDuringBuilds: true,
+ },
};
export default nextConfig;
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 2e80bbb..972c2c1 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -7,6 +7,7 @@
"": {
"name": "frontend",
"version": "0.1.0",
+ "license": "CC-BY-NC-SA-4.0",
"dependencies": {
"@hookform/resolvers": "^5.1.1",
"@radix-ui/react-dialog": "^1.1.14",
@@ -14,6 +15,7 @@
"@radix-ui/react-slot": "^1.2.3",
"@shadcn/ui": "^0.0.4",
"axios": "^1.10.0",
+ "cbor": "^10.0.11",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.515.0",
@@ -2813,6 +2815,18 @@
],
"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": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -5693,6 +5707,15 @@
"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": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 8f78a05..44a32a1 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -23,6 +23,7 @@
"@radix-ui/react-slot": "^1.2.3",
"@shadcn/ui": "^0.0.4",
"axios": "^1.10.0",
+ "cbor": "^10.0.11",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.515.0",
diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx
index 136948e..89b52e4 100644
--- a/frontend/src/app/layout.tsx
+++ b/frontend/src/app/layout.tsx
@@ -14,6 +14,8 @@ import { Inter } from "next/font/google";
import "./globals.css";
import Link from "next/link";
import { MobileNav } from "@/components/mobile-nav";
+import { AuthProvider } from "@/lib/auth/context";
+import { AuthNav } from "@/components/auth/auth-nav";
const inter = Inter({ subsets: ["latin"] });
@@ -37,43 +39,48 @@ export default function RootLayout({
return (
-
-