feature/7-add-wasm-api-in-sharenet-passport-for-creating-and-editing-passports #8

Merged
15 changed files with 3664 additions and 29 deletions
Showing only changes of commit bd4c3ac3ab - Show all commits

View file

@ -81,6 +81,7 @@ where
&passport.univ_id,
&passport.user_profiles,
&passport.date_of_birth,
&passport.default_user_profile_id,
)
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to encrypt file: {}", e.into())))?;
@ -171,6 +172,7 @@ where
&passport.univ_id,
&passport.user_profiles,
&passport.date_of_birth,
&passport.default_user_profile_id,
)
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to encrypt file: {}", e.into())))?;
@ -219,7 +221,7 @@ where
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to load file: {}", e.into())))?;
// Decrypt file
let (seed, public_key, private_key, user_profiles, date_of_birth) = self
let (seed, public_key, private_key, user_profiles, date_of_birth, default_user_profile_id) = self
.file_encryptor
.decrypt(&passport_file, password)
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to decrypt file: {}", e.into())))?;
@ -234,6 +236,7 @@ where
);
passport.user_profiles = user_profiles;
passport.date_of_birth = date_of_birth;
passport.default_user_profile_id = default_user_profile_id;
// Re-encrypt and save if output path provided
if let Some(output_path) = output_path {
@ -247,6 +250,7 @@ where
&passport.univ_id,
&passport.user_profiles,
&passport.date_of_birth,
&passport.default_user_profile_id,
)
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to re-encrypt file: {}", e.into())))?;
@ -296,6 +300,7 @@ where
&passport.univ_id,
&passport.user_profiles,
&passport.date_of_birth,
&passport.default_user_profile_id,
)
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to encrypt file: {}", e.into())))?;
@ -383,6 +388,7 @@ where
&passport.univ_id,
&passport.user_profiles,
&passport.date_of_birth,
&passport.default_user_profile_id,
)
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to encrypt file: {}", e.into())))?;
@ -419,6 +425,7 @@ where
&self,
passport: &mut Passport,
id: Option<&str>,
hub_did: Option<String>,
identity: UserIdentity,
preferences: UserPreferences,
password: &str,
@ -434,10 +441,10 @@ where
let now = time::now_seconds()
.map_err(|e| ApplicationError::UseCaseError(format!("Time error: {}", e)))?;
// Use existing hub_did (cannot change hub_did via update)
// Use provided hub_did or keep existing
let profile = UserProfile {
id: existing_profile.id.clone(),
hub_did: existing_profile.hub_did.clone(),
hub_did: hub_did.or_else(|| existing_profile.hub_did.clone()),
identity,
preferences,
created_at: existing_profile.created_at,
@ -458,6 +465,7 @@ where
&passport.univ_id,
&passport.user_profiles,
&passport.date_of_birth,
&passport.default_user_profile_id,
)
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to encrypt file: {}", e.into())))?;
@ -514,6 +522,7 @@ where
&passport.univ_id,
&passport.user_profiles,
&passport.date_of_birth,
&passport.default_user_profile_id,
)
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to encrypt file: {}", e.into())))?;

View file

@ -233,6 +233,7 @@ mod tests {
let result = update_profile_use_case.execute(
&mut passport,
Some(&profile_id),
Some("h:example".to_string()),
updated_identity,
updated_preferences,
"test-password",
@ -293,6 +294,7 @@ mod tests {
let result = update_profile_use_case.execute(
&mut passport,
Some("non-existent-id"),
Some("h:example".to_string()),
identity,
preferences,
"test-password",

View file

@ -81,6 +81,7 @@ pub struct Passport {
pub univ_id: String,
pub user_profiles: Vec<UserProfile>,
pub date_of_birth: Option<DateOfBirth>,
pub default_user_profile_id: Option<String>, // UUIDv7 of the default user profile
}
impl Passport {
@ -119,8 +120,9 @@ impl Passport {
private_key,
did,
univ_id,
user_profiles: vec![default_profile],
user_profiles: vec![default_profile.clone()],
date_of_birth: None,
default_user_profile_id: Some(default_profile.id.clone()),
}
}
@ -141,7 +143,12 @@ impl Passport {
}
pub fn default_user_profile(&self) -> Option<&UserProfile> {
self.user_profiles.iter().find(|p| p.is_default())
if let Some(default_id) = &self.default_user_profile_id {
self.user_profile_by_id(default_id)
} else {
// Fallback to implicit detection for backward compatibility
self.user_profiles.iter().find(|p| p.is_default())
}
}
pub fn user_profile_for_hub(&self, hub_did: &str) -> Option<&UserProfile> {
@ -157,9 +164,12 @@ impl Passport {
}
pub fn add_user_profile(&mut self, profile: UserProfile) -> Result<(), String> {
// Ensure only one default profile
if profile.is_default() && self.default_user_profile().is_some() {
return Err("Default user profile already exists".to_string());
// If this is a default profile (no hub_did), set it as the default
if profile.hub_did.is_none() {
if self.default_user_profile_id.is_some() {
return Err("Default user profile already exists".to_string());
}
self.default_user_profile_id = Some(profile.id.clone());
}
// Ensure hub_did is unique
@ -225,7 +235,7 @@ impl Passport {
match index {
Some(idx) => {
// Check if this is the default profile
if self.user_profiles[idx].is_default() {
if self.default_user_profile_id.as_deref() == Some(profile_id) {
return Err("Cannot delete default user profile".to_string());
}
self.user_profiles.remove(idx);
@ -234,6 +244,23 @@ impl Passport {
None => Err("User profile not found".to_string()),
}
}
pub fn set_default_user_profile(&mut self, profile_id: &str) -> Result<(), String> {
// Verify the profile exists
if self.user_profile_by_id(profile_id).is_none() {
return Err("User profile not found".to_string());
}
// Verify the profile is a default profile (no hub_did)
if let Some(profile) = self.user_profile_by_id(profile_id) {
if profile.hub_did.is_some() {
return Err("Cannot set hub-specific profile as default".to_string());
}
}
self.default_user_profile_id = Some(profile_id.to_string());
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -310,4 +337,5 @@ pub struct PassportFile {
pub version: String,
pub enc_user_profiles: Vec<u8>, // Encrypted CBOR of Vec<UserProfile>
pub enc_date_of_birth: Vec<u8>, // Encrypted CBOR of Option<DateOfBirth>
pub enc_default_user_profile_id: Vec<u8>, // Encrypted CBOR of Option<String>
}

View file

@ -27,13 +27,14 @@ pub trait FileEncryptor {
univ_id: &str,
user_profiles: &[UserProfile],
date_of_birth: &Option<DateOfBirth>,
default_user_profile_id: &Option<String>,
) -> Result<PassportFile, Self::Error>;
fn decrypt(
&self,
file: &PassportFile,
password: &str,
) -> Result<(Seed, PublicKey, PrivateKey, Vec<UserProfile>, Option<DateOfBirth>), Self::Error>;
) -> Result<(Seed, PublicKey, PrivateKey, Vec<UserProfile>, Option<DateOfBirth>, Option<String>), Self::Error>;
}
pub trait FileStorage {

View file

@ -91,6 +91,7 @@ impl FileEncryptor for XChaCha20FileEncryptor {
univ_id: &str,
user_profiles: &[UserProfile],
date_of_birth: &Option<DateOfBirth>,
default_user_profile_id: &Option<String>,
) -> Result<PassportFile, Self::Error> {
// Generate salt and nonce
let mut salt = [0u8; SALT_LENGTH];
@ -126,6 +127,13 @@ impl FileEncryptor for XChaCha20FileEncryptor {
.encrypt(&nonce, &*date_of_birth_bytes)
.map_err(|e| DomainError::CryptographicError(format!("Date of birth encryption failed: {}", e)))?;
// Serialize and encrypt default user profile ID
let default_user_profile_id_bytes = serde_cbor::to_vec(&default_user_profile_id)
.map_err(|e| DomainError::CryptographicError(format!("Failed to serialize default user profile ID: {}", e)))?;
let enc_default_user_profile_id = cipher
.encrypt(&nonce, &*default_user_profile_id_bytes)
.map_err(|e| DomainError::CryptographicError(format!("Default user profile ID encryption failed: {}", e)))?;
// Get current timestamp
let created_at = SystemTime::now()
.duration_since(UNIX_EPOCH)
@ -145,6 +153,7 @@ impl FileEncryptor for XChaCha20FileEncryptor {
version: "1.0.0".to_string(),
enc_user_profiles,
enc_date_of_birth,
enc_default_user_profile_id,
})
}
@ -152,7 +161,7 @@ impl FileEncryptor for XChaCha20FileEncryptor {
&self,
file: &PassportFile,
password: &str,
) -> Result<(Seed, PublicKey, PrivateKey, Vec<UserProfile>, Option<DateOfBirth>), Self::Error> {
) -> Result<(Seed, PublicKey, PrivateKey, Vec<UserProfile>, Option<DateOfBirth>, Option<String>), Self::Error> {
// Validate file format
validate_file_format(&file.kdf, &file.cipher)?;
@ -196,7 +205,14 @@ impl FileEncryptor for XChaCha20FileEncryptor {
let date_of_birth: Option<DateOfBirth> = serde_cbor::from_slice(&date_of_birth_bytes)
.map_err(|e| DomainError::CryptographicError(format!("Failed to deserialize date of birth: {}", e)))?;
// Decrypt default user profile ID
let default_user_profile_id_bytes = cipher
.decrypt(&nonce, &*file.enc_default_user_profile_id)
.map_err(|e| DomainError::CryptographicError(format!("Default user profile ID decryption failed: {}", e)))?;
let default_user_profile_id: Option<String> = serde_cbor::from_slice(&default_user_profile_id_bytes)
.map_err(|e| DomainError::CryptographicError(format!("Failed to deserialize default user profile ID: {}", e)))?;
// Note: univ_id is stored in the PassportFile and will be used when creating the Passport
Ok((seed, public_key, private_key, user_profiles, date_of_birth))
Ok((seed, public_key, private_key, user_profiles, date_of_birth, default_user_profile_id))
}
}

View file

@ -45,7 +45,7 @@ mod tests {
let password = "test-password";
// Encrypt
let encrypted_file = encryptor.encrypt(&seed, password, &public_key, &did, "u:Test Universe:12345678-1234-1234-1234-123456789012", &[], &None).unwrap();
let encrypted_file = encryptor.encrypt(&seed, password, &public_key, &did, "u:Test Universe:12345678-1234-1234-1234-123456789012", &[], &None, &None).unwrap();
// Verify file structure
assert_eq!(encrypted_file.kdf, "HKDF-SHA256");
@ -56,7 +56,7 @@ mod tests {
assert_eq!(encrypted_file.did, did.0);
// Decrypt
let (decrypted_seed, decrypted_public_key, _, _, _) = encryptor.decrypt(&encrypted_file, password).unwrap();
let (decrypted_seed, decrypted_public_key, _, _, _, _) = encryptor.decrypt(&encrypted_file, password).unwrap();
// Verify decryption
assert_eq!(decrypted_seed.as_bytes(), seed.as_bytes());
@ -72,7 +72,7 @@ mod tests {
let did = Did::new(&public_key);
// Encrypt with one password
let encrypted_file = encryptor.encrypt(&seed, "correct-password", &public_key, &did, "u:Test Universe:12345678-1234-1234-1234-123456789012", &[], &None).unwrap();
let encrypted_file = encryptor.encrypt(&seed, "correct-password", &public_key, &did, "u:Test Universe:12345678-1234-1234-1234-123456789012", &[], &None, &None).unwrap();
// Try to decrypt with wrong password
let result = encryptor.decrypt(&encrypted_file, "wrong-password");

View file

@ -45,7 +45,7 @@ mod tests {
let password = "test-password";
// Encrypt
let encrypted_file = encryptor.encrypt(&seed, password, &public_key, &did, "u:Test Universe:12345678-1234-1234-1234-123456789012", &[]).unwrap();
let encrypted_file = encryptor.encrypt(&seed, password, &public_key, &did, "u:Test Universe:12345678-1234-1234-1234-123456789012", &[], &None, &None).unwrap();
// Verify file structure
assert_eq!(encrypted_file.kdf, "HKDF-SHA256");
@ -56,7 +56,7 @@ mod tests {
assert_eq!(encrypted_file.did, did.0);
// Decrypt
let (decrypted_seed, decrypted_public_key, _, _) = encryptor.decrypt(&encrypted_file, password).unwrap();
let (decrypted_seed, decrypted_public_key, _, _, _, _) = encryptor.decrypt(&encrypted_file, password).unwrap();
// Verify decryption
assert_eq!(decrypted_seed.as_bytes(), seed.as_bytes());
@ -72,7 +72,7 @@ mod tests {
let did = Did::new(&public_key);
// Encrypt with one password
let encrypted_file = encryptor.encrypt(&seed, "correct-password", &public_key, &did, "u:Test Universe:12345678-1234-1234-1234-123456789012", &[]).unwrap();
let encrypted_file = encryptor.encrypt(&seed, "correct-password", &public_key, &did, "u:Test Universe:12345678-1234-1234-1234-123456789012", &[], &None, &None).unwrap();
// Try to decrypt with wrong password
let result = encryptor.decrypt(&encrypted_file, "wrong-password");

View file

@ -26,6 +26,7 @@ mod tests {
version: "1.0.0".to_string(),
enc_user_profiles: vec![],
enc_date_of_birth: vec![],
enc_default_user_profile_id: vec![],
};
// Save the file

View file

@ -24,6 +24,8 @@ mod tests {
created_at: 1234567890,
version: "1.0.0".to_string(),
enc_user_profiles: vec![],
enc_date_of_birth: vec![],
enc_default_user_profile_id: vec![],
};
// Save the file

View file

@ -114,7 +114,7 @@ pub fn import_from_encrypted_data(
// Decrypt the passport file using the password
let encryptor = XChaCha20FileEncryptor;
let (seed, public_key, private_key, user_profiles, date_of_birth) = encryptor.decrypt(
let (seed, public_key, private_key, user_profiles, date_of_birth, default_user_profile_id) = encryptor.decrypt(
&passport_file,
&password,
).map_err(|e| JsValue::from_str(&format!("Failed to decrypt passport: {}", e)))?;
@ -129,6 +129,7 @@ pub fn import_from_encrypted_data(
univ_id: passport_file.univ_id,
user_profiles,
date_of_birth,
default_user_profile_id,
};
let result = serde_wasm_bindgen::to_value(&passport)
@ -157,6 +158,7 @@ pub fn export_to_encrypted_data(
&passport.univ_id,
&passport.user_profiles,
&passport.date_of_birth,
&passport.default_user_profile_id,
).map_err(|e| JsValue::from_str(&format!("Failed to encrypt passport: {}", e)))?;
// Serialize to bytes for browser download

View file

@ -65,6 +65,26 @@ pub enum Commands {
file: String,
},
/// Display complete decrypted Passport data
Show {
/// .spf file path
file: String,
},
/// Edit global Passport fields
Edit {
/// .spf file path
file: String,
/// Date of birth (format: MM-DD-YYYY)
#[arg(long, conflicts_with = "remove_date_of_birth")]
date_of_birth: Option<String>,
/// Remove date of birth
#[arg(long, conflicts_with = "date_of_birth")]
remove_date_of_birth: bool,
},
/// Sign a message (for testing)
Sign {
/// .spf file path
@ -95,7 +115,7 @@ pub enum ProfileCommands {
file: String,
/// Hub DID (optional, omit for default profile)
#[arg(short, long)]
#[arg(long)]
hub_did: Option<String>,
/// Handle
@ -149,8 +169,12 @@ pub enum ProfileCommands {
file: String,
/// Profile ID (required, use 'list' command to see available IDs)
#[arg(short, long)]
id: String,
#[arg(short, long, conflicts_with = "default")]
id: Option<String>,
/// Update the default user profile
#[arg(long, conflicts_with = "id")]
default: bool,
/// Hub DID (optional, can be updated)
#[arg(long)]
@ -199,6 +223,10 @@ pub enum ProfileCommands {
/// Enable auto-sync
#[arg(long)]
auto_sync: Option<bool>,
/// Show date of birth
#[arg(long)]
show_date_of_birth: Option<bool>,
},
/// Delete a user profile

View file

@ -29,6 +29,13 @@ impl CliInterface {
}
pub fn handle_create(&self, universe: &str, output: &str) -> Result<(), ApplicationError> {
// Validate universe ID format
if !universe.starts_with("u:") {
return Err(ApplicationError::UseCaseError(
"Invalid universe ID format. Must start with 'u:'".to_string()
));
}
let password = prompt_password("Enter password for new passport: ")
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to read password: {}", e)))?;
let confirm_password = prompt_password("Confirm password: ")
@ -64,7 +71,22 @@ impl CliInterface {
for i in 1..=24 {
let word = prompt_password(&format!("Word {}: ", i))
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to read recovery word: {}", e)))?;
recovery_words.push(word);
// Validate recovery word is not empty
if word.trim().is_empty() {
return Err(ApplicationError::UseCaseError(
format!("Recovery word {} cannot be empty", i)
));
}
recovery_words.push(word.trim().to_lowercase());
}
// Validate that all words are non-empty
if recovery_words.iter().any(|word| word.is_empty()) {
return Err(ApplicationError::UseCaseError(
"Recovery phrase contains empty words".to_string()
));
}
let password = prompt_password("Enter new password for passport file: ")
@ -165,6 +187,187 @@ impl CliInterface {
Ok(())
}
pub fn handle_show(&self, file: &str) -> Result<(), ApplicationError> {
let password = prompt_password("Enter password for passport file: ")
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to read password: {}", e)))?;
let import_use_case = ImportFromFileUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
let passport = import_use_case.execute(file, &password, None)?;
println!("🔓 Complete Decrypted Passport Data:");
println!(" File: {}", file);
println!(" Universe ID: {}", passport.univ_id());
println!(" DID: {}", passport.did().as_str());
println!(" Public Key: {}", hex::encode(&passport.public_key.0));
println!(" Private Key: {} (⚠️ SENSITIVE - DO NOT SHARE)", hex::encode(&passport.private_key.0));
println!(" Seed: {} (⚠️ SENSITIVE - DO NOT SHARE)", hex::encode(passport.seed.as_bytes()));
if let Some(date_of_birth) = &passport.date_of_birth {
println!(" Date of Birth: {}-{}-{}", date_of_birth.month, date_of_birth.day, date_of_birth.year);
} else {
println!(" Date of Birth: Not set");
}
if let Some(default_profile_id) = &passport.default_user_profile_id {
println!(" Default User Profile ID: {}", default_profile_id);
} else {
println!(" Default User Profile ID: Not set");
}
println!("\n👤 User Profiles ({} total):", passport.user_profiles().len());
for (i, profile) in passport.user_profiles().iter().enumerate() {
println!("\n {}. Profile ID: {}", i + 1, profile.id);
println!(" Profile Type: {}", if profile.is_default() { "Default" } else { "Hub-specific" });
if let Some(hub_did) = &profile.hub_did {
println!(" Hub DID: {}", hub_did);
}
println!(" Created: {}", profile.created_at);
println!(" Updated: {}", profile.updated_at);
println!(" Identity:");
if let Some(handle) = &profile.identity.handle {
println!(" Handle: {}", handle);
}
if let Some(name) = &profile.identity.display_name {
println!(" Display Name: {}", name);
}
if let Some(first_name) = &profile.identity.first_name {
println!(" First Name: {}", first_name);
}
if let Some(last_name) = &profile.identity.last_name {
println!(" Last Name: {}", last_name);
}
if let Some(email) = &profile.identity.email {
println!(" Email: {}", email);
}
if let Some(avatar) = &profile.identity.avatar_url {
println!(" Avatar URL: {}", avatar);
}
if let Some(bio) = &profile.identity.bio {
println!(" Bio: {}", bio);
}
println!(" Preferences:");
if let Some(theme) = &profile.preferences.theme {
println!(" Theme: {}", theme);
}
if let Some(language) = &profile.preferences.language {
println!(" Language: {}", language);
}
println!(" Notifications: {}", if profile.preferences.notifications_enabled { "Enabled" } else { "Disabled" });
println!(" Auto-sync: {}", if profile.preferences.auto_sync { "Enabled" } else { "Disabled" });
println!(" Show Date of Birth: {}", if profile.preferences.show_date_of_birth { "Yes" } else { "No" });
}
println!("\n⚠️ SECURITY WARNING:");
println!(" - Private key and seed are sensitive cryptographic material");
println!(" - Never share these values with anyone");
println!(" - Keep this information secure and confidential");
Ok(())
}
pub fn handle_edit(
&self,
file: &str,
date_of_birth: Option<String>,
remove_date_of_birth: bool,
) -> Result<(), ApplicationError> {
let password = prompt_password("Enter password for passport file: ")
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to read password: {}", e)))?;
let import_use_case = ImportFromFileUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
let mut passport = import_use_case.execute(file, &password, None)?;
let mut changes_made = false;
// Handle date of birth changes
if remove_date_of_birth {
passport.date_of_birth = None;
changes_made = true;
println!("🗑️ Date of birth removed");
} else if let Some(dob_str) = date_of_birth {
// Parse date of birth string (format: MM-DD-YYYY)
let parts: Vec<&str> = dob_str.split('-').collect();
if parts.len() != 3 {
return Err(ApplicationError::UseCaseError(
"Invalid date format. Use MM-DD-YYYY".to_string()
));
}
let month = parts[0].parse::<u8>()
.map_err(|_| ApplicationError::UseCaseError("Invalid month".to_string()))?;
let day = parts[1].parse::<u8>()
.map_err(|_| ApplicationError::UseCaseError("Invalid day".to_string()))?;
let year = parts[2].parse::<u16>()
.map_err(|_| ApplicationError::UseCaseError("Invalid year".to_string()))?;
// Basic validation
if month < 1 || month > 12 {
return Err(ApplicationError::UseCaseError("Month must be between 1 and 12".to_string()));
}
if day < 1 || day > 31 {
return Err(ApplicationError::UseCaseError("Day must be between 1 and 31".to_string()));
}
if year < 1900 || year > 2100 {
return Err(ApplicationError::UseCaseError("Year must be between 1900 and 2100".to_string()));
}
// Comprehensive date validation
let max_days = match month {
2 => {
// February - check for leap year
let is_leap_year = (year % 4 == 0) && (year % 100 != 0 || year % 400 == 0);
if is_leap_year { 29 } else { 28 }
}
4 | 6 | 9 | 11 => 30, // April, June, September, November
_ => 31, // January, March, May, July, August, October, December
};
if day > max_days {
return Err(ApplicationError::UseCaseError(
format!("Invalid day {} for month {}. Maximum days for this month is {}", day, month, max_days)
));
}
let new_dob = sharenet_passport::domain::entities::DateOfBirth {
month,
day,
year,
};
passport.date_of_birth = Some(new_dob);
changes_made = true;
println!("📅 Date of birth set to: {}-{}-{}", month, day, year);
}
if !changes_made {
println!(" No changes specified. Use --date-of-birth or --remove-date-of-birth");
return Ok(());
}
// Save the updated passport
let export_use_case = ExportPassportUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
export_use_case.execute(&passport, &password, file)?;
println!("✅ Passport updated successfully!");
println!("📄 Saved to: {}", file);
Ok(())
}
pub fn handle_sign(&self, file: &str, message: &str) -> Result<(), ApplicationError> {
let password = prompt_password("Enter password for passport file: ")
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to read password: {}", e)))?;
@ -239,6 +442,7 @@ impl CliInterface {
}
println!(" Notifications: {}", if profile.preferences.notifications_enabled { "Enabled" } else { "Disabled" });
println!(" Auto-sync: {}", if profile.preferences.auto_sync { "Enabled" } else { "Disabled" });
println!(" Show Date of Birth: {}", if profile.preferences.show_date_of_birth { "Yes" } else { "No" });
}
Ok(())
@ -315,7 +519,8 @@ impl CliInterface {
pub fn handle_profile_update(
&self,
file: &str,
id: &str,
id: Option<&str>,
default: bool,
hub_did: Option<String>,
handle: Option<String>,
display_name: Option<String>,
@ -328,6 +533,7 @@ impl CliInterface {
language: Option<String>,
notifications: Option<bool>,
auto_sync: Option<bool>,
show_date_of_birth: Option<bool>,
) -> Result<(), ApplicationError> {
let password = prompt_password("Enter password for passport file: ")
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to read password: {}", e)))?;
@ -339,8 +545,23 @@ impl CliInterface {
let mut passport = import_use_case.execute(file, &password, None)?;
// Determine which profile to update and get profile ID
let profile_id = if default {
// Update the default profile
let default_profile = passport.default_user_profile()
.ok_or_else(|| ApplicationError::UseCaseError("Default user profile not found".to_string()))?;
Some(default_profile.id.clone())
} else if let Some(id) = id {
// Update specific profile by ID
Some(id.to_string())
} else {
return Err(ApplicationError::UseCaseError(
"Either --id or --default must be specified".to_string()
));
};
// Get existing profile by ID
let existing_profile = passport.user_profile_by_id(id)
let existing_profile = passport.user_profile_by_id(&profile_id.clone().unwrap())
.ok_or_else(|| ApplicationError::UseCaseError("User profile not found".to_string()))?;
let identity = UserIdentity {
@ -358,13 +579,14 @@ impl CliInterface {
language: language.or_else(|| existing_profile.preferences.language.clone()),
notifications_enabled: notifications.unwrap_or(existing_profile.preferences.notifications_enabled),
auto_sync: auto_sync.unwrap_or(existing_profile.preferences.auto_sync),
show_date_of_birth: existing_profile.preferences.show_date_of_birth,
show_date_of_birth: show_date_of_birth.unwrap_or(existing_profile.preferences.show_date_of_birth),
};
// Clone values before using them in multiple places
let identity_clone = identity.clone();
let preferences_clone = preferences.clone();
let hub_did_clone = hub_did.clone();
let hub_did_for_use_case = hub_did.clone();
// Create updated profile with new hub_did if provided
let now = std::time::SystemTime::now()
@ -389,7 +611,8 @@ impl CliInterface {
update_use_case.execute(
&mut passport,
Some(id),
profile_id.as_deref(),
hub_did_for_use_case,
identity_clone,
preferences_clone,
&password,

View file

@ -1,2 +1,5 @@
pub mod commands;
pub mod interface;
pub mod interface;
#[cfg(test)]
pub mod tests;

File diff suppressed because it is too large Load diff

View file

@ -28,6 +28,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Commands::Info { file } => {
interface.handle_info(&file)?;
}
Commands::Show { file } => {
interface.handle_show(&file)?;
}
Commands::Edit { file, date_of_birth, remove_date_of_birth } => {
interface.handle_edit(&file, date_of_birth, remove_date_of_birth)?;
}
Commands::Sign { file, message } => {
interface.handle_sign(&file, &message)?;
}
@ -70,6 +76,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
crate::cli::commands::ProfileCommands::Update {
file,
id,
default,
hub_did,
handle,
display_name,
@ -82,10 +89,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
language,
notifications,
auto_sync,
show_date_of_birth,
} => {
interface.handle_profile_update(
&file,
&id,
id.as_deref(),
default,
hub_did,
handle,
display_name,
@ -98,6 +107,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
language,
notifications,
auto_sync,
show_date_of_birth,
)?;
}
crate::cli::commands::ProfileCommands::Delete { file, id } => {