Implement shared RFC 5545 VEvent library with workspace restructuring
- Created calendar-models/ shared library with RFC 5545-compliant VEvent structures - Migrated backend to use shared VEvent with proper field mappings (dtstart/dtend, rrule, exdate, etc.) - Converted CalDAV client to parse into VEvent structures with structured types - Updated all CRUD handlers to use VEvent with CalendarUser, Attendee, VAlarm types - Restructured project as Cargo workspace with frontend/, backend/, calendar-models/ - Updated Trunk configuration for new directory structure - Fixed all compilation errors and field references throughout codebase - Updated documentation and build instructions for workspace structure 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
		
							
								
								
									
										284
									
								
								frontend/src/config.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										284
									
								
								frontend/src/config.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,284 @@ | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use std::env; | ||||
| use base64::prelude::*; | ||||
|  | ||||
| /// Configuration for CalDAV server connection and authentication. | ||||
| ///  | ||||
| /// This struct holds all the necessary information to connect to a CalDAV server, | ||||
| /// including server URL, credentials, and optional collection paths. | ||||
| ///  | ||||
| /// # Security Note | ||||
| ///  | ||||
| /// The password field contains sensitive information and should be handled carefully. | ||||
| /// This struct implements `Debug` but in production, consider implementing a custom | ||||
| /// `Debug` that masks the password field. | ||||
| ///  | ||||
| /// # Example | ||||
| ///  | ||||
| /// ```rust | ||||
| /// use crate::config::CalDAVConfig; | ||||
| ///  | ||||
| /// // Load configuration from environment variables | ||||
| /// let config = CalDAVConfig::from_env()?; | ||||
| ///  | ||||
| /// // Use the configuration for HTTP requests | ||||
| /// let auth_header = format!("Basic {}", config.get_basic_auth()); | ||||
| /// ``` | ||||
| #[derive(Debug, Clone, Serialize, Deserialize)] | ||||
| pub struct CalDAVConfig { | ||||
|     /// The base URL of the CalDAV server (e.g., "https://caldav.example.com/dav/") | ||||
|     pub server_url: String, | ||||
|      | ||||
|     /// Username for authentication with the CalDAV server | ||||
|     pub username: String, | ||||
|      | ||||
|     /// Password for authentication with the CalDAV server | ||||
|     ///  | ||||
|     /// **Security Note**: This contains sensitive information | ||||
|     pub password: String, | ||||
|      | ||||
|     /// Optional path to the calendar collection on the server | ||||
|     ///  | ||||
|     /// If not provided, the client will need to discover available calendars | ||||
|     /// through CalDAV PROPFIND requests | ||||
|     pub calendar_path: Option<String>, | ||||
|      | ||||
|     /// Optional path to the tasks/todo collection on the server | ||||
|     ///  | ||||
|     /// Some CalDAV servers store tasks separately from calendar events | ||||
|     pub tasks_path: Option<String>, | ||||
| } | ||||
|  | ||||
| impl CalDAVConfig { | ||||
|     /// Creates a new CalDAVConfig by loading values from environment variables. | ||||
|     ///  | ||||
|     /// This method will attempt to load a `.env` file from the current directory | ||||
|     /// and then read the following required environment variables: | ||||
|     ///  | ||||
|     /// - `CALDAV_SERVER_URL`: The CalDAV server base URL | ||||
|     /// - `CALDAV_USERNAME`: Username for authentication | ||||
|     /// - `CALDAV_PASSWORD`: Password for authentication | ||||
|     ///  | ||||
|     /// Optional environment variables: | ||||
|     ///  | ||||
|     /// - `CALDAV_CALENDAR_PATH`: Path to calendar collection | ||||
|     /// - `CALDAV_TASKS_PATH`: Path to tasks collection | ||||
|     ///  | ||||
|     /// # Errors | ||||
|     ///  | ||||
|     /// Returns `ConfigError::MissingVar` if any required environment variable | ||||
|     /// is not set or cannot be read. | ||||
|     ///  | ||||
|     /// # Example | ||||
|     ///  | ||||
|     /// ```rust | ||||
|     /// use crate::config::CalDAVConfig; | ||||
|     ///  | ||||
|     /// match CalDAVConfig::from_env() { | ||||
|     ///     Ok(config) => { | ||||
|     ///         println!("Loaded config for server: {}", config.server_url); | ||||
|     ///     } | ||||
|     ///     Err(e) => { | ||||
|     ///         eprintln!("Failed to load config: {}", e); | ||||
|     ///     } | ||||
|     /// } | ||||
|     /// ``` | ||||
|     pub fn from_env() -> Result<Self, ConfigError> { | ||||
|         // Attempt to load .env file, but don't fail if it doesn't exist | ||||
|         dotenvy::dotenv().ok(); | ||||
|  | ||||
|         let server_url = env::var("CALDAV_SERVER_URL") | ||||
|             .map_err(|_| ConfigError::MissingVar("CALDAV_SERVER_URL".to_string()))?; | ||||
|  | ||||
|         let username = env::var("CALDAV_USERNAME") | ||||
|             .map_err(|_| ConfigError::MissingVar("CALDAV_USERNAME".to_string()))?; | ||||
|  | ||||
|         let password = env::var("CALDAV_PASSWORD") | ||||
|             .map_err(|_| ConfigError::MissingVar("CALDAV_PASSWORD".to_string()))?; | ||||
|  | ||||
|         // Optional paths - it's fine if these are not set | ||||
|         let calendar_path = env::var("CALDAV_CALENDAR_PATH").ok(); | ||||
|         let tasks_path = env::var("CALDAV_TASKS_PATH").ok(); | ||||
|  | ||||
|         Ok(CalDAVConfig { | ||||
|             server_url, | ||||
|             username, | ||||
|             password, | ||||
|             calendar_path, | ||||
|             tasks_path, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     /// Generates a Base64-encoded string for HTTP Basic Authentication. | ||||
|     ///  | ||||
|     /// This method combines the username and password in the format | ||||
|     /// `username:password` and encodes it using Base64, which is the | ||||
|     /// standard format for the `Authorization: Basic` HTTP header. | ||||
|     ///  | ||||
|     /// # Returns | ||||
|     ///  | ||||
|     /// A Base64-encoded string that can be used directly in the | ||||
|     /// `Authorization` header: `Authorization: Basic <returned_value>` | ||||
|     ///  | ||||
|     /// # Example | ||||
|     ///  | ||||
|     /// ```rust | ||||
|     /// use crate::config::CalDAVConfig; | ||||
|     ///  | ||||
|     /// let config = CalDAVConfig { | ||||
|     ///     server_url: "https://example.com".to_string(), | ||||
|     ///     username: "user".to_string(), | ||||
|     ///     password: "pass".to_string(), | ||||
|     ///     calendar_path: None, | ||||
|     ///     tasks_path: None, | ||||
|     /// }; | ||||
|     ///  | ||||
|     /// let auth_value = config.get_basic_auth(); | ||||
|     /// let auth_header = format!("Basic {}", auth_value); | ||||
|     /// ``` | ||||
|     pub fn get_basic_auth(&self) -> String { | ||||
|         let credentials = format!("{}:{}", self.username, self.password); | ||||
|         BASE64_STANDARD.encode(&credentials) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Errors that can occur when loading or using CalDAV configuration. | ||||
| #[derive(Debug, thiserror::Error)] | ||||
| pub enum ConfigError { | ||||
|     /// A required environment variable is missing or cannot be read. | ||||
|     ///  | ||||
|     /// This error occurs when calling `CalDAVConfig::from_env()` and one of the | ||||
|     /// required environment variables (`CALDAV_SERVER_URL`, `CALDAV_USERNAME`, | ||||
|     /// or `CALDAV_PASSWORD`) is not set. | ||||
|     #[error("Missing environment variable: {0}")] | ||||
|     MissingVar(String), | ||||
|      | ||||
|     /// The configuration contains invalid or malformed values. | ||||
|     ///  | ||||
|     /// This could include malformed URLs, invalid authentication credentials, | ||||
|     /// or other configuration issues that prevent proper CalDAV operation. | ||||
|     #[error("Invalid configuration: {0}")] | ||||
|     Invalid(String), | ||||
| } | ||||
|  | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use super::*; | ||||
|  | ||||
|     #[test] | ||||
|     fn test_basic_auth_encoding() { | ||||
|         let config = CalDAVConfig { | ||||
|             server_url: "https://example.com".to_string(), | ||||
|             username: "testuser".to_string(), | ||||
|             password: "testpass".to_string(), | ||||
|             calendar_path: None, | ||||
|             tasks_path: None, | ||||
|         }; | ||||
|  | ||||
|         let auth = config.get_basic_auth(); | ||||
|         let expected = BASE64_STANDARD.encode("testuser:testpass"); | ||||
|         assert_eq!(auth, expected); | ||||
|     } | ||||
|  | ||||
|     /// Integration test that authenticates with the actual Baikal CalDAV server | ||||
|     ///  | ||||
|     /// This test requires a valid .env file with: | ||||
|     /// - CALDAV_SERVER_URL | ||||
|     /// - CALDAV_USERNAME   | ||||
|     /// - CALDAV_PASSWORD | ||||
|     ///  | ||||
|     /// Run with: `cargo test test_baikal_auth` | ||||
|     #[tokio::test] | ||||
|     async fn test_baikal_auth() { | ||||
|         // Load config from .env | ||||
|         let config = CalDAVConfig::from_env() | ||||
|             .expect("Failed to load CalDAV config from environment"); | ||||
|  | ||||
|         println!("Testing authentication to: {}", config.server_url); | ||||
|  | ||||
|         // Create HTTP client | ||||
|         let client = reqwest::Client::new(); | ||||
|  | ||||
|         // Make a simple OPTIONS request to test authentication | ||||
|         let response = client | ||||
|             .request(reqwest::Method::OPTIONS, &config.server_url) | ||||
|             .header("Authorization", format!("Basic {}", config.get_basic_auth())) | ||||
|             .header("User-Agent", "calendar-app/0.1.0") | ||||
|             .send() | ||||
|             .await | ||||
|             .expect("Failed to send request to CalDAV server"); | ||||
|  | ||||
|         println!("Response status: {}", response.status()); | ||||
|         println!("Response headers: {:#?}", response.headers()); | ||||
|  | ||||
|         // Check if we got a successful response or at least not a 401 Unauthorized | ||||
|         assert!( | ||||
|             response.status().is_success() || response.status() != 401, | ||||
|             "Authentication failed with status: {}. Check your credentials in .env", | ||||
|             response.status() | ||||
|         ); | ||||
|  | ||||
|         // For Baikal/CalDAV servers, we should see DAV headers | ||||
|         assert!( | ||||
|             response.headers().contains_key("dav") ||  | ||||
|             response.headers().contains_key("DAV") || | ||||
|             response.status().is_success(), | ||||
|             "Server doesn't appear to be a CalDAV server - missing DAV headers" | ||||
|         ); | ||||
|  | ||||
|         println!("✓ Authentication test passed!"); | ||||
|     } | ||||
|  | ||||
|     /// Test making a PROPFIND request to discover calendars | ||||
|     ///  | ||||
|     /// This test requires a valid .env file and makes an actual CalDAV PROPFIND request | ||||
|     ///  | ||||
|     /// Run with: `cargo test test_propfind_calendars` | ||||
|     #[tokio::test] | ||||
|     async fn test_propfind_calendars() { | ||||
|         let config = CalDAVConfig::from_env() | ||||
|             .expect("Failed to load CalDAV config from environment"); | ||||
|  | ||||
|         let client = reqwest::Client::new(); | ||||
|  | ||||
|         // CalDAV PROPFIND request to discover calendars | ||||
|         let propfind_body = r#"<?xml version="1.0" encoding="utf-8" ?> | ||||
| <d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav"> | ||||
|     <d:prop> | ||||
|         <d:resourcetype /> | ||||
|         <d:displayname /> | ||||
|         <c:calendar-description /> | ||||
|         <c:supported-calendar-component-set /> | ||||
|     </d:prop> | ||||
| </d:propfind>"#; | ||||
|  | ||||
|         let response = client | ||||
|             .request(reqwest::Method::from_bytes(b"PROPFIND").unwrap(), &config.server_url) | ||||
|             .header("Authorization", format!("Basic {}", config.get_basic_auth())) | ||||
|             .header("Content-Type", "application/xml") | ||||
|             .header("Depth", "1") | ||||
|             .header("User-Agent", "calendar-app/0.1.0") | ||||
|             .body(propfind_body) | ||||
|             .send() | ||||
|             .await | ||||
|             .expect("Failed to send PROPFIND request"); | ||||
|  | ||||
|         let status = response.status(); | ||||
|         println!("PROPFIND Response status: {}", status); | ||||
|          | ||||
|         let body = response.text().await.expect("Failed to read response body"); | ||||
|         println!("PROPFIND Response body: {}", body); | ||||
|  | ||||
|         // We should get a 207 Multi-Status for PROPFIND | ||||
|         assert_eq!( | ||||
|             status, | ||||
|             reqwest::StatusCode::from_u16(207).unwrap(), | ||||
|             "PROPFIND should return 207 Multi-Status" | ||||
|         ); | ||||
|  | ||||
|         // The response should contain XML with calendar information | ||||
|         assert!(body.contains("calendar"), "Response should contain calendar information"); | ||||
|  | ||||
|         println!("✓ PROPFIND calendars test passed!"); | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Connor Johnstone
					Connor Johnstone