Some checks failed
Build and Push Docker Image / docker (push) Failing after 1m7s
Moved event fetching logic from CalendarView to Calendar component to properly use the visible date range instead of hardcoded current month. The Calendar component already tracks the current visible date through navigation, so events now load correctly for August and other months when navigating. Changes: - Calendar component now manages its own events state and fetching - Event fetching responds to current_date changes from navigation - CalendarView simplified to just render Calendar component - Fixed cargo fmt/clippy formatting across codebase 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
270 lines
8.9 KiB
Rust
270 lines
8.9 KiB
Rust
use base64::prelude::*;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::env;
|
|
|
|
/// 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 calendar_backend::config::CalDAVConfig;
|
|
/// let config = CalDAVConfig {
|
|
/// server_url: "https://caldav.example.com".to_string(),
|
|
/// username: "user@example.com".to_string(),
|
|
/// password: "password".to_string(),
|
|
/// calendar_path: None,
|
|
/// tasks_path: None,
|
|
/// };
|
|
///
|
|
/// // 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 discover available calendars
|
|
/// through CalDAV PROPFIND requests
|
|
pub calendar_path: Option<String>,
|
|
}
|
|
|
|
impl CalDAVConfig {
|
|
/// Creates a new CalDAVConfig with the given credentials.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `server_url` - The base URL of the CalDAV server
|
|
/// * `username` - Username for authentication
|
|
/// * `password` - Password for authentication
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```rust
|
|
/// # use calendar_backend::config::CalDAVConfig;
|
|
/// let config = CalDAVConfig::new(
|
|
/// "https://caldav.example.com".to_string(),
|
|
/// "user@example.com".to_string(),
|
|
/// "password".to_string()
|
|
/// );
|
|
/// ```
|
|
pub fn new(server_url: String, username: String, password: String) -> Self {
|
|
Self {
|
|
server_url,
|
|
username,
|
|
password,
|
|
calendar_path: env::var("CALDAV_CALENDAR_PATH").ok(), // Optional override from env
|
|
}
|
|
}
|
|
|
|
/// 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 calendar_backend::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,
|
|
};
|
|
|
|
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() {
|
|
// Use test config - update these values to test with real server
|
|
let config = CalDAVConfig::new(
|
|
"https://example.com".to_string(),
|
|
"test_user".to_string(),
|
|
"test_password".to_string(),
|
|
);
|
|
|
|
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() {
|
|
// Use test config - update these values to test with real server
|
|
let config = CalDAVConfig::new(
|
|
"https://example.com".to_string(),
|
|
"test_user".to_string(),
|
|
"test_password".to_string(),
|
|
);
|
|
|
|
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!");
|
|
}
|
|
}
|