- 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>
284 lines
9.8 KiB
Rust
284 lines
9.8 KiB
Rust
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!");
|
|
}
|
|
} |