diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a713c3eb..36dbbb55 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -43,7 +43,7 @@ "@babel/preset-react": "^7.26.3", "@babel/preset-typescript": "^7.26.0", "@jest/types": "^29.6.3", - "@tauri-apps/cli": "^2.0.0-beta.21", + "@tauri-apps/cli": "^2.2.7", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", "@testing-library/user-event": "^14.6.1", diff --git a/frontend/package.json b/frontend/package.json index befadf3b..8c4644c2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -47,12 +47,12 @@ "vite-plugin-environment": "^1.1.3" }, "devDependencies": { + "@babel/plugin-proposal-private-property-in-object": "^7.21", "@babel/preset-env": "^7.26.7", "@babel/preset-react": "^7.26.3", "@babel/preset-typescript": "^7.26.0", - "@babel/plugin-proposal-private-property-in-object": "^7.21", "@jest/types": "^29.6.3", - "@tauri-apps/cli": "^2.0.0-beta.21", + "@tauri-apps/cli": "^2.2.7", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", "@testing-library/user-event": "^14.6.1", diff --git a/frontend/src-tauri/Cargo.lock b/frontend/src-tauri/Cargo.lock index 15ae31bb..ff994c06 100644 --- a/frontend/src-tauri/Cargo.lock +++ b/frontend/src-tauri/Cargo.lock @@ -7,6 +7,7 @@ name = "PictoPy" version = "0.0.0" dependencies = [ "anyhow", + "argon2", "arrayref", "base64 0.21.7", "chrono", @@ -92,6 +93,18 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -297,6 +310,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bit_field" version = "0.10.2" @@ -318,6 +337,15 @@ dependencies = [ "serde", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block" version = "0.1.6" @@ -819,6 +847,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -1076,6 +1105,27 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "file-id" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bc904b9bbefcadbd8e3a9fb0d464a9b979de6324c03b3c663e8994f46a5be36" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "filetime" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.4.1", + "windows-sys 0.52.0", +] + [[package]] name = "flate2" version = "1.0.30" @@ -1128,6 +1178,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "futf" version = "0.1.5" @@ -1789,6 +1848,26 @@ dependencies = [ "cfb", ] +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "instant" version = "0.1.13" @@ -1920,6 +1999,26 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "kqueue" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + [[package]] name = "kuchikiki" version = "0.8.2" @@ -2129,6 +2228,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", + "log", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.48.0", ] @@ -2205,6 +2305,40 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.5.0", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "serde", + "walkdir", + "windows-sys 0.48.0", +] + +[[package]] +name = "notify-debouncer-full" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb7fd166739789c9ff169e654dc1501373db9d80a4c3f972817c8a4d7cf8f34e" +dependencies = [ + "crossbeam-channel", + "file-id", + "log", + "notify", + "parking_lot", + "walkdir", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -2507,11 +2641,22 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.1", "smallvec", "windows-targets 0.52.5", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "pathdiff" version = "0.2.1" @@ -2966,6 +3111,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.5.1" @@ -3473,7 +3627,7 @@ dependencies = [ "objc2-foundation", "objc2-quartz-core", "raw-window-handle 0.6.2", - "redox_syscall", + "redox_syscall 0.5.1", "wasm-bindgen", "web-sys", "windows-sys 0.52.0", @@ -3564,6 +3718,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "swift-rs" version = "1.0.6" @@ -3828,6 +3988,8 @@ checksum = "17e1f9fb6054cbc48c9e0fda7e8c8aeaccddb4fe880163473555beeed580010e" dependencies = [ "anyhow", "glob", + "notify", + "notify-debouncer-full", "schemars", "serde", "serde_json", diff --git a/frontend/src-tauri/Cargo.toml b/frontend/src-tauri/Cargo.toml index 5f377ff1..3362dcbe 100644 --- a/frontend/src-tauri/Cargo.toml +++ b/frontend/src-tauri/Cargo.toml @@ -17,18 +17,19 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1" anyhow = "1.0" image = "0.24.6" -tauri-plugin-fs = "2.0.0-beta.7" -tauri-plugin-shell = "2.0.0-beta.5" -tauri-plugin-dialog = "2.0.0-beta.9" +tauri-plugin-fs = { version = "2.0.0-beta", features = ["watch"] } +tauri-plugin-shell = "2.0.0-beta" +tauri-plugin-dialog = "2.0.0-beta" tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } -ring = "0.16.20" +ring = { version = "0.16.20", features = ["std"] } data-encoding = "2.3.2" arrayref = "0.3.6" directories = "4.0" chrono = { version = "0.4.26", features = ["serde"] } +argon2 = "0.5.3" base64 = "0.21.0" -rand = "0.8.5" +rand = { version = "0.8", features = ["std"] } [features] # This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!! diff --git a/frontend/src-tauri/capabilities/main.json b/frontend/src-tauri/capabilities/main.json new file mode 100644 index 00000000..3143718a --- /dev/null +++ b/frontend/src-tauri/capabilities/main.json @@ -0,0 +1,31 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "main-capability", + "description": "Capability for the main window", + "windows": ["main"], + "permissions": [ + "path:default", + "event:default", + "window:default", + "app:default", + "resources:default", + "menu:default", + "tray:default", + "shell:allow-open", + "dialog:allow-open", + "dialog:allow-save", + "fs:default", + "fs:scope", + "fs:allow-read", + "fs:allow-write", + "fs:allow-app-write", + "fs:allow-app-write-recursive", + "fs:allow-appcache-write", + "fs:allow-appcache-write-recursive", + "fs:allow-appconfig-write", + "fs:allow-appconfig-write-recursive", + "fs:allow-download-write", + "fs:allow-download-write-recursive", + "store:default" + ] +} \ No newline at end of file diff --git a/frontend/src-tauri/src/services/mod.rs b/frontend/src-tauri/src/services/mod.rs index 533f9f1b..a4d59325 100644 --- a/frontend/src-tauri/src/services/mod.rs +++ b/frontend/src-tauri/src/services/mod.rs @@ -8,19 +8,25 @@ pub use cache_service::CacheService; use chrono::{DateTime, Datelike, Utc}; use data_encoding::BASE64; use serde::{Serialize, Deserialize}; -use std::num::NonZeroU32; -use ring::rand::{SystemRandom, SecureRandom}; -use ring::digest; -use ring::pbkdf2; +use argon2::{ + password_hash::{ + rand_core::OsRng, + SaltString + }, + Argon2 +}; use ring::aead::{ Aad, LessSafeKey, Nonce, UnboundKey, AES_256_GCM}; +use ring::rand::{SystemRandom, SecureRandom}; // Add SecureRandom trait pub use file_service::FileService; use image::{DynamicImage, GenericImageView, ImageBuffer, Rgba}; use tauri::path::BaseDirectory; use tauri::Manager; -use std::fs; -use directories::ProjectDirs; +use std::fs; +use directories::ProjectDirs; use rand::seq::SliceRandom; -use std::collections::HashSet; +use std::collections::HashSet; +use std::fs::OpenOptions; +use std::io::{Seek, SeekFrom, Write}; const SECURE_FOLDER_NAME: &str = "secure_folder"; const SALT_LENGTH: usize = 16; @@ -329,48 +335,47 @@ fn get_secure_folder_path() -> Result { } fn generate_salt() -> [u8; SALT_LENGTH] { + let salt_string = SaltString::generate(&mut OsRng); let mut salt = [0u8; SALT_LENGTH]; - SystemRandom::new().fill(&mut salt).unwrap(); + salt.copy_from_slice(&salt_string.as_ref().as_bytes()[..SALT_LENGTH]); salt } #[tauri::command] pub async fn move_to_secure_folder(path: String, password: String) -> Result<(), String> { + // Validate password first + validate_password(&password)?; + + let path_clone = path.clone(); // Clone path for later use let secure_folder = get_secure_folder_path()?; - let file_name = Path::new(&path).file_name().ok_or("Invalid file name")?; + let file_name = Path::new(&path_clone).file_name().ok_or("Invalid file name")?; let dest_path = secure_folder.join(file_name); - let content = fs::read(&path).map_err(|e| e.to_string())?; - let ciphertext_length = content.len() + AES_256_GCM.tag_len(); - let _expected_length = SALT_LENGTH + NONCE_LENGTH + ciphertext_length + 16; + let content = fs::read(&path_clone).map_err(|e| e.to_string())?; let encrypted = encrypt_data(&content, &password).map_err(|e| e.to_string())?; - println!("Encrypted file size: {}", encrypted.len()); - fs::write(&dest_path, encrypted).map_err(|e| e.to_string())?; - println!("Encrypted file saved to: {:?}", secure_folder); - fs::remove_file(&path).map_err(|e| e.to_string())?; - - let thumbnails_folder = Path::new(&path) - .parent() // Get parent directory of the original file - .and_then(|parent| parent.join("PictoPy.thumbnails").canonicalize().ok()) // Navigate to PictoPy.thumbnails - .ok_or("Unable to locate thumbnails directory")?; - let thumbnail_path = thumbnails_folder.join(file_name); - - if thumbnail_path.exists() { - fs::remove_file(&thumbnail_path).map_err(|e| e.to_string())?; - println!("Thumbnail deleted: {:?}", thumbnail_path); - } else { - println!("Thumbnail not found: {:?}", thumbnail_path); - } + + // Write to temporary file first + let temp_path = dest_path.with_extension("tmp"); + fs::write(&temp_path, &encrypted).map_err(|e| e.to_string())?; + + // Rename temporary file to final destination + fs::rename(&temp_path, &dest_path).map_err(|e| e.to_string())?; + + // Securely delete the original file + secure_delete_file(path_clone.clone()).await?; - // Store the original path + // Update metadata let metadata_path = secure_folder.join("metadata.json"); let mut metadata: HashMap = if metadata_path.exists() { serde_json::from_str(&fs::read_to_string(&metadata_path).map_err(|e| e.to_string())?).map_err(|e| e.to_string())? } else { HashMap::new() }; - metadata.insert(file_name.to_string_lossy().to_string(), path); - fs::write(&metadata_path, serde_json::to_string(&metadata).unwrap()).map_err(|e| e.to_string())?; + + metadata.insert(file_name.to_string_lossy().to_string(), path); // Use original path here + + fs::write(&metadata_path, serde_json::to_string(&metadata).unwrap()) + .map_err(|e| e.to_string())?; Ok(()) } @@ -383,20 +388,30 @@ pub async fn remove_from_secure_folder(file_name: String, password: String) -> R // Read and decrypt the file let encrypted_content = fs::read(&file_path).map_err(|e| e.to_string())?; - let decrypted_content = decrypt_data(&encrypted_content, &password).map_err(|e| e.to_string())?; + let decrypted_content = decrypt_data(&encrypted_content, &password)?; // Get the original path - let metadata: HashMap = serde_json::from_str(&fs::read_to_string(&metadata_path).map_err(|e| e.to_string())?).map_err(|e| e.to_string())?; + let metadata: HashMap = serde_json::from_str( + &fs::read_to_string(&metadata_path).map_err(|e| e.to_string())? + ).map_err(|e| e.to_string())?; + let original_path = metadata.get(&file_name).ok_or("Original path not found")?; - // Write the decrypted content back to the original path - fs::write(original_path, decrypted_content).map_err(|e| e.to_string())?; + // Write to temporary file first + let temp_path = PathBuf::from(original_path).with_extension("tmp"); + fs::write(&temp_path, &decrypted_content).map_err(|e| e.to_string())?; + + // Rename temporary file to final destination + fs::rename(&temp_path, original_path).map_err(|e| e.to_string())?; + + // Securely delete the encrypted file + secure_delete_file(file_path.to_string_lossy().to_string()).await?; - // Remove the file from the secure folder and update metadata - fs::remove_file(&file_path).map_err(|e| e.to_string())?; + // Update metadata let mut updated_metadata = metadata; updated_metadata.remove(&file_name); - fs::write(&metadata_path, serde_json::to_string(&updated_metadata).unwrap()).map_err(|e| e.to_string())?; + fs::write(&metadata_path, serde_json::to_string(&updated_metadata).unwrap()) + .map_err(|e| e.to_string())?; Ok(()) } @@ -456,14 +471,24 @@ pub async fn get_secure_media(password: String) -> Result, Stri } fn hash_password(password: &str, salt: &[u8]) -> Vec { - let mut hash = [0u8; digest::SHA256_OUTPUT_LEN]; - pbkdf2::derive( - pbkdf2::PBKDF2_HMAC_SHA256, - NonZeroU32::new(100_000).unwrap(), - salt, - password.as_bytes(), - &mut hash, + let mut hash = [0u8; 32]; // 32 bytes for the hash + let config = Argon2::new( + argon2::Algorithm::Argon2id, + argon2::Version::V0x13, + argon2::Params::new( + 65536, // 64MB memory cost + 2, // 2 iterations + 1, // 1 degree of parallelism (good for desktop apps) + Some(32) // output length + ).unwrap() ); + + config.hash_password_into( + password.as_bytes(), + salt, + &mut hash + ).expect("Argon2id hashing failed"); + hash.to_vec() } @@ -489,10 +514,9 @@ fn encrypt_data(data: &[u8], password: &str) -> Result, ring::error::Uns } fn decrypt_data(encrypted: &[u8], password: &str) -> Result, String> { - println!("Decrypting data..."); - if encrypted.len() < SALT_LENGTH + NONCE_LENGTH + 16 { - return Err(format!("Encrypted data too short: {} bytes", encrypted.len())); + println!("Technical error: Data length {} is invalid", encrypted.len()); + return Err("Unable to decrypt file - data appears to be corrupted".to_string()); } let salt = &encrypted[..SALT_LENGTH]; @@ -511,11 +535,12 @@ fn decrypt_data(encrypted: &[u8], password: &str) -> Result, String> { match key.open_in_place(nonce, Aad::empty(), &mut plaintext) { Ok(decrypted) => { - println!("Decryption successful! Decrypted length: {}", decrypted.len()); + println!("Decryption successful! Length: {}", decrypted.len()); Ok(decrypted.to_vec()) }, Err(e) => { - Err(format!("Decryption error: {:?}", e)) + println!("Decryption technical error: {:?}", e); + Err("Unable to decrypt file - incorrect password or corrupted data".to_string()) } } } @@ -541,14 +566,23 @@ pub async fn unlock_secure_folder(password: String) -> Result { fn derive_key(password: &str, salt: &[u8]) -> LessSafeKey { let mut key_bytes = [0u8; 32]; - pbkdf2::derive( - pbkdf2::PBKDF2_HMAC_SHA256, - NonZeroU32::new(100_000).unwrap(), - salt, - password.as_bytes(), - &mut key_bytes, + let config = Argon2::new( + argon2::Algorithm::Argon2id, + argon2::Version::V0x13, + argon2::Params::new( + 65536, // 64MB memory cost + 2, // 2 iterations + 1, // 1 degree of parallelism (good for desktop apps) + Some(32) // output length + ).unwrap() ); + config.hash_password_into( + password.as_bytes(), + salt, + &mut key_bytes + ).expect("Argon2id hashing failed"); + let unbound_key = UnboundKey::new(&AES_256_GCM, &key_bytes).unwrap(); LessSafeKey::new(unbound_key) } @@ -562,7 +596,8 @@ pub async fn check_secure_folder_status() -> Result { fn generate_nonce() -> [u8; NONCE_LENGTH] { let mut nonce = [0u8; NONCE_LENGTH]; - SystemRandom::new().fill(&mut nonce).unwrap(); + let rng = SystemRandom::new(); + rng.fill(&mut nonce).unwrap(); nonce } @@ -641,3 +676,61 @@ pub fn get_server_path(handle: tauri::AppHandle) -> Result { .map_err(|e| e.to_string())?; Ok(resource_path.to_string_lossy().to_string()) } + +#[tauri::command] +pub async fn secure_delete_file(path: String) -> Result<(), String> { + let path = Path::new(&path); + let metadata = fs::metadata(&path).map_err(|e| e.to_string())?; + let file_size = metadata.len() as usize; + + // Multiple overwrite passes with different patterns + let patterns: &[&[u8]] = &[ + &[0x00], // zeros + &[0xFF], // ones + &[0x55], // alternating 01 + &[0xAA], // alternating 10 + &[0xF0], // random-like pattern + ]; + + for pattern in patterns { + let mut file = OpenOptions::new() + .write(true) + .open(&path) + .map_err(|e| e.to_string())?; + + file.seek(SeekFrom::Start(0)) + .map_err(|e| e.to_string())?; + + let buffer = vec![pattern[0]; 8192]; // 8KB buffer + let mut remaining = file_size; + + while remaining > 0 { + let to_write = remaining.min(buffer.len()); + file.write_all(&buffer[..to_write]) + .map_err(|e| e.to_string())?; + remaining = remaining.saturating_sub(to_write); + } + } + + fs::remove_file(path).map_err(|e| e.to_string())?; + Ok(()) +} + +fn validate_password(password: &str) -> Result<(), String> { + if password.len() < 8 { + return Err("Password must be at least 8 characters long".to_string()); + } + + let has_uppercase = password.chars().any(|c| c.is_uppercase()); + let has_digit = password.chars().any(|c| c.is_digit(10)); + + if !has_uppercase { + return Err("Password must contain at least one uppercase letter".to_string()); + } + + if !has_digit { + return Err("Password must contain at least one number".to_string()); + } + + Ok(()) +} diff --git a/frontend/src-tauri/tauri.conf.json b/frontend/src-tauri/tauri.conf.json index 79c3645c..aa0a247e 100644 --- a/frontend/src-tauri/tauri.conf.json +++ b/frontend/src-tauri/tauri.conf.json @@ -29,8 +29,12 @@ "identifier": "com.yourcompany.pictopy", "plugins": { "fs": { - "active": true - } + "scope": ["**"] + }, + "shell": { + "open": true + }, + "dialog": null }, "app": { "windows": [ @@ -40,15 +44,17 @@ "height": 600, "resizable": true, "fullscreen": false, - "maximized": false + "decorations": true, + "label": "main" } ], "security": { + "csp": null, + "capabilities": ["main-capability"], "assetProtocol": { - "scope": ["**"], - "enable": true - }, - "csp": null + "enable": true, + "scope": ["**"] + } } } } diff --git a/frontend/src/Config/Backend.ts b/frontend/src/Config/Backend.ts index 3cf7aee0..83c732b8 100644 --- a/frontend/src/Config/Backend.ts +++ b/frontend/src/Config/Backend.ts @@ -1 +1 @@ -export const BACKED_URL = process.env.BACKEND_URL || 'http://localhost:8000'; +export const BACKED_URL = 'http://localhost:8000/'; \ No newline at end of file diff --git a/frontend/src/components/SecureFolder/SecureFolder.tsx b/frontend/src/components/SecureFolder/SecureFolder.tsx index 4a071395..c56e9028 100644 --- a/frontend/src/components/SecureFolder/SecureFolder.tsx +++ b/frontend/src/components/SecureFolder/SecureFolder.tsx @@ -10,6 +10,22 @@ interface SecureMedia { path: string; } +const validatePassword = (password: string): string | null => { + if (password.length < 8) { + return 'Password must be at least 8 characters long'; + } + + if (!/[A-Z]/.test(password)) { + return 'Password must contain at least one uppercase letter'; + } + + if (!/[0-9]/.test(password)) { + return 'Password must contain at least one number'; + } + + return null; +}; + const SecureFolder: React.FC = () => { const [isSetup, setIsSetup] = useState(false); const [password, setPassword] = useState(''); @@ -40,8 +56,9 @@ const SecureFolder: React.FC = () => { return; } - if (password.length < 4) { - setError('Password must be at least 4 characters long'); + const validationError = validatePassword(password); + if (validationError) { + setError(validationError); return; } diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 2a0c59cd..99133f50 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -18,8 +18,11 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( , ); +// Set up close listener regardless of environment +onCloseListener(); + +// Only start the server in production if (isProd()) { - onCloseListener(); console.log('Starting PictoPy Server'); startServer(); }