Add lots more tests
Some checks are pending
Sharenet Passport CI / test-native (push) Waiting to run
Sharenet Passport CI / test-wasm-headless (push) Waiting to run
Sharenet Passport CI / test-wasm-webdriver (push) Waiting to run
Sharenet Passport CI / build-wasm (push) Waiting to run
Sharenet Passport CI / lint (push) Waiting to run
Some checks are pending
Sharenet Passport CI / test-native (push) Waiting to run
Sharenet Passport CI / test-wasm-headless (push) Waiting to run
Sharenet Passport CI / test-wasm-webdriver (push) Waiting to run
Sharenet Passport CI / build-wasm (push) Waiting to run
Sharenet Passport CI / lint (push) Waiting to run
This commit is contained in:
parent
8d0d203182
commit
bd4c3ac3ab
15 changed files with 3664 additions and 29 deletions
|
|
@ -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())))?;
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,2 +1,5 @@
|
|||
pub mod commands;
|
||||
pub mod interface;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests;
|
||||
3310
sharenet-passport-cli/src/cli/tests.rs
Normal file
3310
sharenet-passport-cli/src/cli/tests.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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 } => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue