Clean up obsolete CalDAV environment variables
Some checks failed
Build and Push Docker Image / docker (push) Failing after 2s

## Removed Obsolete Environment Variables:
- `CALDAV_SERVER_URL` - provided by user login
- `CALDAV_USERNAME` - provided by user login
- `CALDAV_PASSWORD` - provided by user login
- `CALDAV_TASKS_PATH` - not used in any features

## Kept with Intelligent Discovery:
- `CALDAV_CALENDAR_PATH` - optional override, defaults to smart discovery

## Changes:
### Backend
- Remove `CalDAVConfig::from_env()` method (not used in main app)
- Add `CalDAVConfig::new()` constructor with credentials
- Remove `tasks_path` field from CalDAVConfig
- Update auth service to use new constructor
- Update tests to use hardcoded test values instead of env vars
- Update debug tools to use test credentials

### Frontend
- Remove unused `config.rs` file entirely (frontend uses backend API)

## Current Authentication Flow:
1. User provides CalDAV credentials via login API
2. Backend creates CalDAVConfig dynamically from login request
3. Backend tests authentication via calendar discovery
4. Optional `CALDAV_CALENDAR_PATH` env var can override discovery
5. No environment variables required for normal operation

This simplifies deployment - users only need to provide CalDAV
credentials through the web interface, no server-side configuration required.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Connor Johnstone
2025-08-31 19:48:13 -04:00
parent 1fa3bf44b6
commit e55e6bf4dd
6 changed files with 69 additions and 384 deletions

View File

@@ -31,13 +31,11 @@ impl AuthService {
println!("✅ Input validation passed"); println!("✅ Input validation passed");
// Create CalDAV config with provided credentials // Create CalDAV config with provided credentials
let caldav_config = CalDAVConfig { let caldav_config = CalDAVConfig::new(
server_url: request.server_url.clone(), request.server_url.clone(),
username: request.username.clone(), request.username.clone(),
password: request.password.clone(), request.password.clone()
calendar_path: None, );
tasks_path: None,
};
println!("📝 Created CalDAV config"); println!("📝 Created CalDAV config");
// Test authentication against CalDAV server // Test authentication against CalDAV server
@@ -74,13 +72,11 @@ impl AuthService {
pub fn caldav_config_from_token(&self, token: &str, password: &str) -> Result<CalDAVConfig, ApiError> { pub fn caldav_config_from_token(&self, token: &str, password: &str) -> Result<CalDAVConfig, ApiError> {
let claims = self.verify_token(token)?; let claims = self.verify_token(token)?;
Ok(CalDAVConfig { Ok(CalDAVConfig::new(
server_url: claims.server_url, claims.server_url,
username: claims.username, claims.username,
password: password.to_string(), password.to_string()
calendar_path: None, ))
tasks_path: None,
})
} }
fn validate_login(&self, request: &CalDAVLoginRequest) -> Result<(), ApiError> { fn validate_login(&self, request: &CalDAVLoginRequest) -> Result<(), ApiError> {

View File

@@ -17,14 +17,16 @@ use base64::prelude::*;
/// ///
/// ```rust /// ```rust
/// # use calendar_backend::config::CalDAVConfig; /// # use calendar_backend::config::CalDAVConfig;
/// # fn example() -> Result<(), Box<dyn std::error::Error>> { /// let config = CalDAVConfig {
/// // Load configuration from environment variables /// server_url: "https://caldav.example.com".to_string(),
/// let config = CalDAVConfig::from_env()?; /// username: "user@example.com".to_string(),
/// password: "password".to_string(),
/// calendar_path: None,
/// tasks_path: None,
/// };
/// ///
/// // Use the configuration for HTTP requests /// // Use the configuration for HTTP requests
/// let auth_header = format!("Basic {}", config.get_basic_auth()); /// let auth_header = format!("Basic {}", config.get_basic_auth());
/// # Ok(())
/// # }
/// ``` /// ```
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CalDAVConfig { pub struct CalDAVConfig {
@@ -41,74 +43,37 @@ pub struct CalDAVConfig {
/// Optional path to the calendar collection on the server /// Optional path to the calendar collection on the server
/// ///
/// If not provided, the client will need to discover available calendars /// If not provided, the client will discover available calendars
/// through CalDAV PROPFIND requests /// through CalDAV PROPFIND requests
pub calendar_path: Option<String>, 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 { impl CalDAVConfig {
/// Creates a new CalDAVConfig by loading values from environment variables. /// Creates a new CalDAVConfig with the given credentials.
/// ///
/// This method will attempt to load a `.env` file from the current directory /// # Arguments
/// and then read the following required environment variables:
/// ///
/// - `CALDAV_SERVER_URL`: The CalDAV server base URL /// * `server_url` - The base URL of the CalDAV server
/// - `CALDAV_USERNAME`: Username for authentication /// * `username` - Username for authentication
/// - `CALDAV_PASSWORD`: Password for authentication /// * `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 /// # Example
/// ///
/// ```rust /// ```rust
/// # use calendar_backend::config::CalDAVConfig; /// # use calendar_backend::config::CalDAVConfig;
/// /// let config = CalDAVConfig::new(
/// match CalDAVConfig::from_env() { /// "https://caldav.example.com".to_string(),
/// Ok(config) => { /// "user@example.com".to_string(),
/// println!("Loaded config for server: {}", config.server_url); /// "password".to_string()
/// } /// );
/// Err(e) => {
/// eprintln!("Failed to load config: {}", e);
/// }
/// }
/// ``` /// ```
pub fn from_env() -> Result<Self, ConfigError> { pub fn new(server_url: String, username: String, password: String) -> Self {
// Attempt to load .env file, but don't fail if it doesn't exist Self {
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, server_url,
username, username,
password, password,
calendar_path, calendar_path: env::var("CALDAV_CALENDAR_PATH").ok(), // Optional override from env
tasks_path, }
})
} }
/// Generates a Base64-encoded string for HTTP Basic Authentication. /// Generates a Base64-encoded string for HTTP Basic Authentication.
@@ -192,9 +157,12 @@ mod tests {
/// Run with: `cargo test test_baikal_auth` /// Run with: `cargo test test_baikal_auth`
#[tokio::test] #[tokio::test]
async fn test_baikal_auth() { async fn test_baikal_auth() {
// Load config from .env // Use test config - update these values to test with real server
let config = CalDAVConfig::from_env() let config = CalDAVConfig::new(
.expect("Failed to load CalDAV config from environment"); "https://example.com".to_string(),
"test_user".to_string(),
"test_password".to_string()
);
println!("Testing authentication to: {}", config.server_url); println!("Testing authentication to: {}", config.server_url);
@@ -238,8 +206,12 @@ mod tests {
/// Run with: `cargo test test_propfind_calendars` /// Run with: `cargo test test_propfind_calendars`
#[tokio::test] #[tokio::test]
async fn test_propfind_calendars() { async fn test_propfind_calendars() {
let config = CalDAVConfig::from_env() // Use test config - update these values to test with real server
.expect("Failed to load CalDAV config from environment"); let config = CalDAVConfig::new(
"https://example.com".to_string(),
"test_user".to_string(),
"test_password".to_string()
);
let client = reqwest::Client::new(); let client = reqwest::Client::new();

View File

@@ -2,7 +2,12 @@ use crate::calendar::CalDAVClient;
use crate::config::CalDAVConfig; use crate::config::CalDAVConfig;
pub async fn debug_caldav_fetch() -> Result<(), Box<dyn std::error::Error>> { pub async fn debug_caldav_fetch() -> Result<(), Box<dyn std::error::Error>> {
let config = CalDAVConfig::from_env()?; // Use debug/test configuration
let config = CalDAVConfig::new(
"https://example.com".to_string(),
"debug_user".to_string(),
"debug_password".to_string()
);
let client = CalDAVClient::new(config); let client = CalDAVClient::new(config);
println!("=== DEBUG: CalDAV Fetch ==="); println!("=== DEBUG: CalDAV Fetch ===");

View File

@@ -52,13 +52,11 @@ pub async fn login(
println!("📝 Created CalDAV config"); println!("📝 Created CalDAV config");
// First verify the credentials are valid by attempting to discover calendars // First verify the credentials are valid by attempting to discover calendars
let config = CalDAVConfig { let config = CalDAVConfig::new(
server_url: request.server_url.clone(), request.server_url.clone(),
username: request.username.clone(), request.username.clone(),
password: request.password.clone(), request.password.clone()
calendar_path: None, );
tasks_path: None,
};
let client = CalDAVClient::new(config); let client = CalDAVClient::new(config);
client.discover_calendars() client.discover_calendars()
.await .await

View File

@@ -72,9 +72,9 @@ mod test_utils {
pub async fn login(&self) -> String { pub async fn login(&self) -> String {
let login_payload = json!({ let login_payload = json!({
"username": std::env::var("CALDAV_USERNAME").unwrap_or("test".to_string()), "username": "test".to_string(),
"password": std::env::var("CALDAV_PASSWORD").unwrap_or("test".to_string()), "password": "test".to_string(),
"server_url": std::env::var("CALDAV_SERVER_URL").unwrap_or("https://example.com".to_string()) "server_url": "https://example.com".to_string()
}); });
let response = self.client let response = self.client
@@ -134,12 +134,10 @@ mod tests {
async fn test_auth_login() { async fn test_auth_login() {
let server = TestServer::start().await; let server = TestServer::start().await;
// Load credentials from .env // Use test credentials
dotenvy::dotenv().ok(); let username = "test".to_string();
let username = std::env::var("CALDAV_USERNAME").unwrap_or("test".to_string()); let password = "test".to_string();
let password = std::env::var("CALDAV_PASSWORD").unwrap_or("test".to_string()); let server_url = "https://example.com".to_string();
let server_url = std::env::var("CALDAV_SERVER_URL").unwrap_or("https://example.com".to_string());
let login_payload = json!({ let login_payload = json!({
"username": username, "username": username,
@@ -196,7 +194,7 @@ mod tests {
// Load password from env for CalDAV requests // Load password from env for CalDAV requests
dotenvy::dotenv().ok(); dotenvy::dotenv().ok();
let password = std::env::var("CALDAV_PASSWORD").unwrap_or("test".to_string()); let password = "test".to_string();
let response = server.client let response = server.client
.get(&format!("{}/api/user/info", server.base_url)) .get(&format!("{}/api/user/info", server.base_url))
@@ -226,7 +224,7 @@ mod tests {
// Load password from env for CalDAV requests // Load password from env for CalDAV requests
dotenvy::dotenv().ok(); dotenvy::dotenv().ok();
let password = std::env::var("CALDAV_PASSWORD").unwrap_or("test".to_string()); let password = "test".to_string();
let response = server.client let response = server.client
.get(&format!("{}/api/calendar/events?year=2024&month=12", server.base_url)) .get(&format!("{}/api/calendar/events?year=2024&month=12", server.base_url))
@@ -254,7 +252,7 @@ mod tests {
// Load password from env for CalDAV requests // Load password from env for CalDAV requests
dotenvy::dotenv().ok(); dotenvy::dotenv().ok();
let password = std::env::var("CALDAV_PASSWORD").unwrap_or("test".to_string()); let password = "test".to_string();
let create_payload = json!({ let create_payload = json!({
"title": "Integration Test Event", "title": "Integration Test Event",
@@ -308,7 +306,7 @@ mod tests {
// Load password from env for CalDAV requests // Load password from env for CalDAV requests
dotenvy::dotenv().ok(); dotenvy::dotenv().ok();
let password = std::env::var("CALDAV_PASSWORD").unwrap_or("test".to_string()); let password = "test".to_string();
// Use a dummy UID for testing - this will likely return 404 but we're testing the endpoint structure // Use a dummy UID for testing - this will likely return 404 but we're testing the endpoint structure
let test_uid = "test-event-uid"; let test_uid = "test-event-uid";
@@ -373,7 +371,7 @@ mod tests {
// Load password from env for CalDAV requests // Load password from env for CalDAV requests
dotenvy::dotenv().ok(); dotenvy::dotenv().ok();
let password = std::env::var("CALDAV_PASSWORD").unwrap_or("test".to_string()); let password = "test".to_string();
let create_payload = json!({ let create_payload = json!({
"title": "Integration Test Series", "title": "Integration Test Series",
@@ -431,7 +429,7 @@ mod tests {
// Load password from env for CalDAV requests // Load password from env for CalDAV requests
dotenvy::dotenv().ok(); dotenvy::dotenv().ok();
let password = std::env::var("CALDAV_PASSWORD").unwrap_or("test".to_string()); let password = "test".to_string();
let update_payload = json!({ let update_payload = json!({
"series_uid": "test-series-uid", "series_uid": "test-series-uid",
@@ -493,7 +491,7 @@ mod tests {
// Load password from env for CalDAV requests // Load password from env for CalDAV requests
dotenvy::dotenv().ok(); dotenvy::dotenv().ok();
let password = std::env::var("CALDAV_PASSWORD").unwrap_or("test".to_string()); let password = "test".to_string();
let delete_payload = json!({ let delete_payload = json!({
"series_uid": "test-series-to-delete", "series_uid": "test-series-to-delete",

View File

@@ -1,284 +0,0 @@
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!");
}
}