Fix calendar event fetching to use visible date range
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>
This commit is contained in:
Connor Johnstone
2025-09-01 18:31:51 -04:00
parent e55e6bf4dd
commit 79f287ed61
38 changed files with 3922 additions and 2590 deletions

View File

@@ -1,33 +1,38 @@
use axum::{
extract::State,
http::HeaderMap,
response::Json,
};
use axum::{extract::State, http::HeaderMap, response::Json};
use std::sync::Arc;
use crate::{AppState, models::{CalDAVLoginRequest, AuthResponse, ApiError, UserInfo, CalendarInfo}};
use crate::calendar::CalDAVClient;
use crate::config::CalDAVConfig;
use crate::{
models::{ApiError, AuthResponse, CalDAVLoginRequest, CalendarInfo, UserInfo},
AppState,
};
pub fn extract_bearer_token(headers: &HeaderMap) -> Result<String, ApiError> {
let auth_header = headers.get("authorization")
let auth_header = headers
.get("authorization")
.ok_or_else(|| ApiError::Unauthorized("Missing Authorization header".to_string()))?;
let auth_str = auth_header.to_str()
let auth_str = auth_header
.to_str()
.map_err(|_| ApiError::BadRequest("Invalid Authorization header".to_string()))?;
if let Some(token) = auth_str.strip_prefix("Bearer ") {
Ok(token.to_string())
} else {
Err(ApiError::BadRequest("Authorization header must be Bearer token".to_string()))
Err(ApiError::BadRequest(
"Authorization header must be Bearer token".to_string(),
))
}
}
pub fn extract_password_header(headers: &HeaderMap) -> Result<String, ApiError> {
let password_header = headers.get("x-caldav-password")
let password_header = headers
.get("x-caldav-password")
.ok_or_else(|| ApiError::BadRequest("Missing X-CalDAV-Password header".to_string()))?;
password_header.to_str()
password_header
.to_str()
.map(|s| s.to_string())
.map_err(|_| ApiError::BadRequest("Invalid X-CalDAV-Password header".to_string()))
}
@@ -40,32 +45,37 @@ pub async fn login(
println!(" Server URL: {}", request.server_url);
println!(" Username: {}", request.username);
println!(" Password length: {}", request.password.len());
// Basic validation
if request.username.is_empty() || request.password.is_empty() || request.server_url.is_empty() {
return Err(ApiError::BadRequest("Username, password, and server URL are required".to_string()));
return Err(ApiError::BadRequest(
"Username, password, and server URL are required".to_string(),
));
}
println!("✅ Input validation passed");
// Create a token using the auth service
println!("📝 Created CalDAV config");
// First verify the credentials are valid by attempting to discover calendars
let config = CalDAVConfig::new(
request.server_url.clone(),
request.username.clone(),
request.password.clone()
request.password.clone(),
);
let client = CalDAVClient::new(config);
client.discover_calendars()
client
.discover_calendars()
.await
.map_err(|e| ApiError::Unauthorized(format!("Authentication failed: {}", e)))?;
let token = state.auth_service.generate_token(&request.username, &request.server_url)?;
let token = state
.auth_service
.generate_token(&request.username, &request.server_url)?;
println!("🔗 Created CalDAV client, attempting to discover calendars...");
Ok(Json(AuthResponse {
token,
username: request.username,
@@ -79,7 +89,7 @@ pub async fn verify_token(
) -> Result<Json<serde_json::Value>, ApiError> {
let token = extract_bearer_token(&headers)?;
let is_valid = state.auth_service.verify_token(&token).is_ok();
Ok(Json(serde_json::json!({ "valid": is_valid })))
}
@@ -89,26 +99,33 @@ pub async fn get_user_info(
) -> Result<Json<UserInfo>, ApiError> {
let token = extract_bearer_token(&headers)?;
let password = extract_password_header(&headers)?;
// Create CalDAV config from token and password
let config = state.auth_service.caldav_config_from_token(&token, &password)?;
let config = state
.auth_service
.caldav_config_from_token(&token, &password)?;
let client = CalDAVClient::new(config.clone());
// Discover calendars
let calendar_paths = client.discover_calendars()
let calendar_paths = client
.discover_calendars()
.await
.map_err(|e| ApiError::Internal(format!("Failed to discover calendars: {}", e)))?;
println!("✅ Authentication successful! Found {} calendars", calendar_paths.len());
let calendars: Vec<CalendarInfo> = calendar_paths.iter().map(|path| {
CalendarInfo {
println!(
"✅ Authentication successful! Found {} calendars",
calendar_paths.len()
);
let calendars: Vec<CalendarInfo> = calendar_paths
.iter()
.map(|path| CalendarInfo {
path: path.clone(),
display_name: extract_calendar_name(path),
color: generate_calendar_color(path),
}
}).collect();
})
.collect();
Ok(Json(UserInfo {
username: config.username,
server_url: config.server_url,
@@ -123,15 +140,14 @@ fn generate_calendar_color(path: &str) -> String {
for byte in path.bytes() {
hash = hash.wrapping_mul(31).wrapping_add(byte as u32);
}
// Define a set of pleasant colors
let colors = [
"#3B82F6", "#10B981", "#F59E0B", "#EF4444", "#8B5CF6",
"#06B6D4", "#84CC16", "#F97316", "#EC4899", "#6366F1",
"#14B8A6", "#F3B806", "#8B5A2B", "#6B7280", "#DC2626",
"#7C3AED", "#059669", "#D97706", "#BE185D", "#4F46E5"
"#3B82F6", "#10B981", "#F59E0B", "#EF4444", "#8B5CF6", "#06B6D4", "#84CC16", "#F97316",
"#EC4899", "#6366F1", "#14B8A6", "#F3B806", "#8B5A2B", "#6B7280", "#DC2626", "#7C3AED",
"#059669", "#D97706", "#BE185D", "#4F46E5",
];
colors[(hash as usize) % colors.len()].to_string()
}
@@ -154,4 +170,4 @@ fn extract_calendar_name(path: &str) -> String {
})
.collect::<Vec<String>>()
.join(" ")
}
}

View File

@@ -1,12 +1,14 @@
use axum::{
extract::State,
http::HeaderMap,
response::Json,
};
use axum::{extract::State, http::HeaderMap, response::Json};
use std::sync::Arc;
use crate::{AppState, models::{ApiError, CreateCalendarRequest, CreateCalendarResponse, DeleteCalendarRequest, DeleteCalendarResponse}};
use crate::calendar::CalDAVClient;
use crate::{
models::{
ApiError, CreateCalendarRequest, CreateCalendarResponse, DeleteCalendarRequest,
DeleteCalendarResponse,
},
AppState,
};
use super::auth::{extract_bearer_token, extract_password_header};
@@ -20,22 +22,36 @@ pub async fn create_calendar(
// Validate request
if request.name.trim().is_empty() {
return Err(ApiError::BadRequest("Calendar name is required".to_string()));
return Err(ApiError::BadRequest(
"Calendar name is required".to_string(),
));
}
// Create CalDAV config from token and password
let config = state.auth_service.caldav_config_from_token(&token, &password)?;
let config = state
.auth_service
.caldav_config_from_token(&token, &password)?;
let client = CalDAVClient::new(config);
// Create calendar on CalDAV server
match client.create_calendar(&request.name, request.description.as_deref(), request.color.as_deref()).await {
match client
.create_calendar(
&request.name,
request.description.as_deref(),
request.color.as_deref(),
)
.await
{
Ok(_) => Ok(Json(CreateCalendarResponse {
success: true,
message: "Calendar created successfully".to_string(),
})),
Err(e) => {
eprintln!("Failed to create calendar: {}", e);
Err(ApiError::Internal(format!("Failed to create calendar: {}", e)))
Err(ApiError::Internal(format!(
"Failed to create calendar: {}",
e
)))
}
}
}
@@ -50,11 +66,15 @@ pub async fn delete_calendar(
// Validate request
if request.path.trim().is_empty() {
return Err(ApiError::BadRequest("Calendar path is required".to_string()));
return Err(ApiError::BadRequest(
"Calendar path is required".to_string(),
));
}
// Create CalDAV config from token and password
let config = state.auth_service.caldav_config_from_token(&token, &password)?;
let config = state
.auth_service
.caldav_config_from_token(&token, &password)?;
let client = CalDAVClient::new(config);
// Delete calendar on CalDAV server
@@ -65,7 +85,10 @@ pub async fn delete_calendar(
})),
Err(e) => {
eprintln!("Failed to delete calendar: {}", e);
Err(ApiError::Internal(format!("Failed to delete calendar: {}", e)))
Err(ApiError::Internal(format!(
"Failed to delete calendar: {}",
e
)))
}
}
}
}

View File

@@ -1,15 +1,23 @@
use axum::{
extract::{State, Query, Path},
extract::{Path, Query, State},
http::HeaderMap,
response::Json,
};
use chrono::Datelike;
use serde::Deserialize;
use std::sync::Arc;
use chrono::Datelike;
use calendar_models::{VEvent, EventStatus, EventClass, CalendarUser, Attendee, VAlarm, AlarmAction, AlarmTrigger};
use crate::{AppState, models::{ApiError, DeleteEventRequest, DeleteEventResponse, CreateEventRequest, CreateEventResponse, UpdateEventRequest, UpdateEventResponse}};
use crate::calendar::{CalDAVClient, CalendarEvent};
use crate::{
models::{
ApiError, CreateEventRequest, CreateEventResponse, DeleteEventRequest, DeleteEventResponse,
UpdateEventRequest, UpdateEventResponse,
},
AppState,
};
use calendar_models::{
AlarmAction, AlarmTrigger, Attendee, CalendarUser, EventClass, EventStatus, VAlarm, VEvent,
};
use super::auth::{extract_bearer_token, extract_password_header};
@@ -28,20 +36,23 @@ pub async fn get_calendar_events(
let token = extract_bearer_token(&headers)?;
let password = extract_password_header(&headers)?;
println!("🔑 API call with password length: {}", password.len());
// Create CalDAV config from token and password
let config = state.auth_service.caldav_config_from_token(&token, &password)?;
let config = state
.auth_service
.caldav_config_from_token(&token, &password)?;
let client = CalDAVClient::new(config);
// Discover calendars if needed
let calendar_paths = client.discover_calendars()
let calendar_paths = client
.discover_calendars()
.await
.map_err(|e| ApiError::Internal(format!("Failed to discover calendars: {}", e)))?;
if calendar_paths.is_empty() {
return Ok(Json(vec![])); // No calendars found
}
// Fetch events from all calendars
let mut all_events = Vec::new();
for calendar_path in &calendar_paths {
@@ -54,12 +65,15 @@ pub async fn get_calendar_events(
all_events.extend(events);
}
Err(e) => {
eprintln!("Failed to fetch events from calendar {}: {}", calendar_path, e);
eprintln!(
"Failed to fetch events from calendar {}: {}",
calendar_path, e
);
// Continue with other calendars instead of failing completely
}
}
}
// If year and month are specified, filter events
if let (Some(year), Some(month)) = (params.year, params.month) {
all_events.retain(|event| {
@@ -68,7 +82,7 @@ pub async fn get_calendar_events(
event_year == year && event_month == month
});
}
println!("📅 Returning {} events", all_events.len());
Ok(Json(all_events))
}
@@ -80,16 +94,19 @@ pub async fn refresh_event(
) -> Result<Json<Option<CalendarEvent>>, ApiError> {
let token = extract_bearer_token(&headers)?;
let password = extract_password_header(&headers)?;
// Create CalDAV config from token and password
let config = state.auth_service.caldav_config_from_token(&token, &password)?;
let config = state
.auth_service
.caldav_config_from_token(&token, &password)?;
let client = CalDAVClient::new(config);
// Discover calendars
let calendar_paths = client.discover_calendars()
let calendar_paths = client
.discover_calendars()
.await
.map_err(|e| ApiError::Internal(format!("Failed to discover calendars: {}", e)))?;
// Search for the event by UID across all calendars
for calendar_path in &calendar_paths {
if let Ok(Some(mut event)) = client.fetch_event_by_uid(calendar_path, &uid).await {
@@ -97,18 +114,25 @@ pub async fn refresh_event(
return Ok(Json(Some(event)));
}
}
Ok(Json(None))
}
async fn fetch_event_by_href(client: &CalDAVClient, calendar_path: &str, event_href: &str) -> Result<Option<CalendarEvent>, crate::calendar::CalDAVError> {
async fn fetch_event_by_href(
client: &CalDAVClient,
calendar_path: &str,
event_href: &str,
) -> Result<Option<CalendarEvent>, crate::calendar::CalDAVError> {
// This is a simplified implementation - in a real scenario, you'd want to fetch the specific event by href
// For now, we'll fetch all events and find the matching one by href (inefficient but functional)
let events = client.fetch_events(calendar_path).await?;
println!("🔍 fetch_event_by_href: looking for href='{}'", event_href);
println!("🔍 Available events with hrefs: {:?}", events.iter().map(|e| (&e.uid, &e.href)).collect::<Vec<_>>());
println!(
"🔍 Available events with hrefs: {:?}",
events.iter().map(|e| (&e.uid, &e.href)).collect::<Vec<_>>()
);
// First try to match by exact href
for event in &events {
if let Some(stored_href) = &event.href {
@@ -118,22 +142,25 @@ async fn fetch_event_by_href(client: &CalDAVClient, calendar_path: &str, event_h
}
}
}
// Fallback: try to match by UID extracted from href filename
let filename = event_href.split('/').last().unwrap_or(event_href);
let uid_from_href = filename.trim_end_matches(".ics");
println!("🔍 Fallback: trying UID match. filename='{}', uid='{}'", filename, uid_from_href);
println!(
"🔍 Fallback: trying UID match. filename='{}', uid='{}'",
filename, uid_from_href
);
for event in events {
if event.uid == uid_from_href {
println!("✅ Found matching event by UID: {}", event.uid);
return Ok(Some(event));
}
}
println!("❌ No matching event found for href: {}", event_href);
Ok(None)
}
@@ -146,41 +173,63 @@ pub async fn delete_event(
let password = extract_password_header(&headers)?;
// Create CalDAV config from token and password
let config = state.auth_service.caldav_config_from_token(&token, &password)?;
let config = state
.auth_service
.caldav_config_from_token(&token, &password)?;
let client = CalDAVClient::new(config);
// Handle different delete actions for recurring events
match request.delete_action.as_str() {
"delete_this" => {
if let Some(event) = fetch_event_by_href(&client, &request.calendar_path, &request.event_href).await
.map_err(|e| ApiError::Internal(format!("Failed to fetch event: {}", e)))? {
if let Some(event) =
fetch_event_by_href(&client, &request.calendar_path, &request.event_href)
.await
.map_err(|e| ApiError::Internal(format!("Failed to fetch event: {}", e)))?
{
// Check if this is a recurring event
if event.rrule.is_some() && !event.rrule.as_ref().unwrap().is_empty() {
// Recurring event - add EXDATE for this occurrence
if let Some(occurrence_date) = &request.occurrence_date {
let exception_utc = if let Ok(date) = chrono::DateTime::parse_from_rfc3339(occurrence_date) {
let exception_utc = if let Ok(date) =
chrono::DateTime::parse_from_rfc3339(occurrence_date)
{
// RFC3339 format (with time and timezone)
date.with_timezone(&chrono::Utc)
} else if let Ok(naive_date) = chrono::NaiveDate::parse_from_str(occurrence_date, "%Y-%m-%d") {
} else if let Ok(naive_date) =
chrono::NaiveDate::parse_from_str(occurrence_date, "%Y-%m-%d")
{
// Simple date format (YYYY-MM-DD)
naive_date.and_hms_opt(0, 0, 0).unwrap().and_utc()
} else {
return Err(ApiError::BadRequest(format!("Invalid occurrence date format: {}. Expected RFC3339 or YYYY-MM-DD", occurrence_date)));
};
let mut updated_event = event;
updated_event.exdate.push(exception_utc);
println!("🔄 Adding EXDATE {} to recurring event {}", exception_utc.format("%Y%m%dT%H%M%SZ"), updated_event.uid);
println!(
"🔄 Adding EXDATE {} to recurring event {}",
exception_utc.format("%Y%m%dT%H%M%SZ"),
updated_event.uid
);
// Update the event with the new EXDATE
client.update_event(&request.calendar_path, &updated_event, &request.event_href)
client
.update_event(
&request.calendar_path,
&updated_event,
&request.event_href,
)
.await
.map_err(|e| ApiError::Internal(format!("Failed to update event with EXDATE: {}", e)))?;
.map_err(|e| {
ApiError::Internal(format!(
"Failed to update event with EXDATE: {}",
e
))
})?;
println!("✅ Successfully updated recurring event with EXDATE");
Ok(Json(DeleteEventResponse {
success: true,
message: "Single occurrence deleted successfully".to_string(),
@@ -191,13 +240,16 @@ pub async fn delete_event(
} else {
// Non-recurring event - delete the entire event
println!("🗑️ Deleting non-recurring event: {}", event.uid);
client.delete_event(&request.calendar_path, &request.event_href)
client
.delete_event(&request.calendar_path, &request.event_href)
.await
.map_err(|e| ApiError::Internal(format!("Failed to delete event: {}", e)))?;
.map_err(|e| {
ApiError::Internal(format!("Failed to delete event: {}", e))
})?;
println!("✅ Successfully deleted non-recurring event");
Ok(Json(DeleteEventResponse {
success: true,
message: "Event deleted successfully".to_string(),
@@ -206,70 +258,99 @@ pub async fn delete_event(
} else {
Err(ApiError::NotFound("Event not found".to_string()))
}
},
}
"delete_following" => {
// For "this and following" deletion, we need to:
// 1. Fetch the recurring event
// 2. Modify the RRULE to end before this occurrence
// 3. Update the event
if let Some(mut event) = fetch_event_by_href(&client, &request.calendar_path, &request.event_href).await
.map_err(|e| ApiError::Internal(format!("Failed to fetch event: {}", e)))? {
if let Some(mut event) =
fetch_event_by_href(&client, &request.calendar_path, &request.event_href)
.await
.map_err(|e| ApiError::Internal(format!("Failed to fetch event: {}", e)))?
{
if let Some(occurrence_date) = &request.occurrence_date {
let until_date = if let Ok(date) = chrono::DateTime::parse_from_rfc3339(occurrence_date) {
let until_date = if let Ok(date) =
chrono::DateTime::parse_from_rfc3339(occurrence_date)
{
// RFC3339 format (with time and timezone)
date.with_timezone(&chrono::Utc)
} else if let Ok(naive_date) = chrono::NaiveDate::parse_from_str(occurrence_date, "%Y-%m-%d") {
} else if let Ok(naive_date) =
chrono::NaiveDate::parse_from_str(occurrence_date, "%Y-%m-%d")
{
// Simple date format (YYYY-MM-DD)
naive_date.and_hms_opt(0, 0, 0).unwrap().and_utc()
} else {
return Err(ApiError::BadRequest(format!("Invalid occurrence date format: {}. Expected RFC3339 or YYYY-MM-DD", occurrence_date)));
return Err(ApiError::BadRequest(format!(
"Invalid occurrence date format: {}. Expected RFC3339 or YYYY-MM-DD",
occurrence_date
)));
};
// Modify the RRULE to add an UNTIL clause
if let Some(rrule) = &event.rrule {
// Remove existing UNTIL if present and add new one
let parts: Vec<&str> = rrule.split(';').filter(|part| {
!part.starts_with("UNTIL=") && !part.starts_with("COUNT=")
}).collect();
let new_rrule = format!("{};UNTIL={}", parts.join(";"), until_date.format("%Y%m%dT%H%M%SZ"));
let parts: Vec<&str> = rrule
.split(';')
.filter(|part| {
!part.starts_with("UNTIL=") && !part.starts_with("COUNT=")
})
.collect();
let new_rrule = format!(
"{};UNTIL={}",
parts.join(";"),
until_date.format("%Y%m%dT%H%M%SZ")
);
event.rrule = Some(new_rrule);
// Update the event with the modified RRULE
client.update_event(&request.calendar_path, &event, &request.event_href)
client
.update_event(&request.calendar_path, &event, &request.event_href)
.await
.map_err(|e| ApiError::Internal(format!("Failed to update event with modified RRULE: {}", e)))?;
.map_err(|e| {
ApiError::Internal(format!(
"Failed to update event with modified RRULE: {}",
e
))
})?;
Ok(Json(DeleteEventResponse {
success: true,
message: "This and following occurrences deleted successfully".to_string(),
message: "This and following occurrences deleted successfully"
.to_string(),
}))
} else {
// No RRULE, just delete the single event
client.delete_event(&request.calendar_path, &request.event_href)
client
.delete_event(&request.calendar_path, &request.event_href)
.await
.map_err(|e| ApiError::Internal(format!("Failed to delete event: {}", e)))?;
.map_err(|e| {
ApiError::Internal(format!("Failed to delete event: {}", e))
})?;
Ok(Json(DeleteEventResponse {
success: true,
message: "Event deleted successfully".to_string(),
}))
}
} else {
Err(ApiError::BadRequest("Occurrence date is required for following deletion".to_string()))
Err(ApiError::BadRequest(
"Occurrence date is required for following deletion".to_string(),
))
}
} else {
Err(ApiError::NotFound("Event not found".to_string()))
}
},
}
"delete_series" | _ => {
// Delete the entire event/series
client.delete_event(&request.calendar_path, &request.event_href)
client
.delete_event(&request.calendar_path, &request.event_href)
.await
.map_err(|e| ApiError::Internal(format!("Failed to delete event: {}", e)))?;
Ok(Json(DeleteEventResponse {
success: true,
message: "Event deleted successfully".to_string(),
@@ -283,9 +364,11 @@ pub async fn create_event(
headers: HeaderMap,
Json(request): Json<CreateEventRequest>,
) -> Result<Json<CreateEventResponse>, ApiError> {
println!("📝 Create event request received: title='{}', all_day={}, calendar_path={:?}",
request.title, request.all_day, request.calendar_path);
println!(
"📝 Create event request received: title='{}', all_day={}, calendar_path={:?}",
request.title, request.all_day, request.calendar_path
);
// Extract and verify token
let token = extract_bearer_token(&headers)?;
let password = extract_password_header(&headers)?;
@@ -294,13 +377,17 @@ pub async fn create_event(
if request.title.trim().is_empty() {
return Err(ApiError::BadRequest("Event title is required".to_string()));
}
if request.title.len() > 200 {
return Err(ApiError::BadRequest("Event title too long (max 200 characters)".to_string()));
return Err(ApiError::BadRequest(
"Event title too long (max 200 characters)".to_string(),
));
}
// Create CalDAV config from token and password
let config = state.auth_service.caldav_config_from_token(&token, &password)?;
let config = state
.auth_service
.caldav_config_from_token(&token, &password)?;
let client = CalDAVClient::new(config);
// Determine which calendar to use
@@ -308,31 +395,41 @@ pub async fn create_event(
path
} else {
// Use the first available calendar
let calendar_paths = client.discover_calendars()
let calendar_paths = client
.discover_calendars()
.await
.map_err(|e| ApiError::Internal(format!("Failed to discover calendars: {}", e)))?;
if calendar_paths.is_empty() {
return Err(ApiError::BadRequest("No calendars available for event creation".to_string()));
return Err(ApiError::BadRequest(
"No calendars available for event creation".to_string(),
));
}
calendar_paths[0].clone()
};
// Parse dates and times
let start_datetime = parse_event_datetime(&request.start_date, &request.start_time, request.all_day)
.map_err(|e| ApiError::BadRequest(format!("Invalid start date/time: {}", e)))?;
let start_datetime =
parse_event_datetime(&request.start_date, &request.start_time, request.all_day)
.map_err(|e| ApiError::BadRequest(format!("Invalid start date/time: {}", e)))?;
let end_datetime = parse_event_datetime(&request.end_date, &request.end_time, request.all_day)
.map_err(|e| ApiError::BadRequest(format!("Invalid end date/time: {}", e)))?;
// Validate that end is after start
if end_datetime <= start_datetime {
return Err(ApiError::BadRequest("End date/time must be after start date/time".to_string()));
return Err(ApiError::BadRequest(
"End date/time must be after start date/time".to_string(),
));
}
// Generate a unique UID for the event
let uid = format!("{}-{}", uuid::Uuid::new_v4(), chrono::Utc::now().timestamp());
let uid = format!(
"{}-{}",
uuid::Uuid::new_v4(),
chrono::Utc::now().timestamp()
);
// Parse status
let status = match request.status.to_lowercase().as_str() {
@@ -352,7 +449,8 @@ pub async fn create_event(
let attendees: Vec<String> = if request.attendees.trim().is_empty() {
Vec::new()
} else {
request.attendees
request
.attendees
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
@@ -363,7 +461,8 @@ pub async fn create_event(
let categories: Vec<String> = if request.categories.trim().is_empty() {
Vec::new()
} else {
request.categories
request
.categories
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
@@ -399,10 +498,11 @@ pub async fn create_event(
"WEEKLY" => {
// Handle weekly recurrence with optional BYDAY parameter
let mut rrule = "FREQ=WEEKLY".to_string();
// Check if specific days are selected (recurrence_days has 7 elements: [Sun, Mon, Tue, Wed, Thu, Fri, Sat])
if request.recurrence_days.len() == 7 {
let selected_days: Vec<&str> = request.recurrence_days
let selected_days: Vec<&str> = request
.recurrence_days
.iter()
.enumerate()
.filter_map(|(i, &selected)| {
@@ -416,20 +516,20 @@ pub async fn create_event(
5 => "FR", // Friday
6 => "SA", // Saturday
_ => return None,
})
} else {
None
}
})
.collect();
})
} else {
None
}
})
.collect();
if !selected_days.is_empty() {
rrule = format!("{};BYDAY={}", rrule, selected_days.join(","));
if !selected_days.is_empty() {
rrule = format!("{};BYDAY={}", rrule, selected_days.join(","));
}
}
}
Some(rrule)
},
}
"MONTHLY" => Some("FREQ=MONTHLY".to_string()),
"YEARLY" => Some("FREQ=YEARLY".to_string()),
_ => None,
@@ -439,15 +539,27 @@ pub async fn create_event(
// Create the VEvent struct (RFC 5545 compliant)
let mut event = VEvent::new(uid, start_datetime);
event.dtend = Some(end_datetime);
event.summary = if request.title.trim().is_empty() { None } else { Some(request.title.clone()) };
event.description = if request.description.trim().is_empty() { None } else { Some(request.description) };
event.location = if request.location.trim().is_empty() { None } else { Some(request.location) };
event.summary = if request.title.trim().is_empty() {
None
} else {
Some(request.title.clone())
};
event.description = if request.description.trim().is_empty() {
None
} else {
Some(request.description)
};
event.location = if request.location.trim().is_empty() {
None
} else {
Some(request.location)
};
event.status = Some(status);
event.class = Some(class);
event.priority = request.priority;
event.organizer = if request.organizer.trim().is_empty() {
None
} else {
event.organizer = if request.organizer.trim().is_empty() {
None
} else {
Some(CalendarUser {
cal_address: request.organizer,
common_name: None,
@@ -456,41 +568,53 @@ pub async fn create_event(
language: None,
})
};
event.attendees = attendees.into_iter().map(|email| Attendee {
cal_address: email,
common_name: None,
role: None,
part_stat: None,
rsvp: None,
cu_type: None,
member: Vec::new(),
delegated_to: Vec::new(),
delegated_from: Vec::new(),
sent_by: None,
dir_entry_ref: None,
language: None,
}).collect();
event.attendees = attendees
.into_iter()
.map(|email| Attendee {
cal_address: email,
common_name: None,
role: None,
part_stat: None,
rsvp: None,
cu_type: None,
member: Vec::new(),
delegated_to: Vec::new(),
delegated_from: Vec::new(),
sent_by: None,
dir_entry_ref: None,
language: None,
})
.collect();
event.categories = categories;
event.rrule = rrule;
event.all_day = request.all_day;
event.alarms = alarms.into_iter().map(|reminder| VAlarm {
action: AlarmAction::Display,
trigger: AlarmTrigger::Duration(chrono::Duration::minutes(-reminder.minutes_before as i64)),
duration: None,
repeat: None,
description: reminder.description,
summary: None,
attendees: Vec::new(),
attach: Vec::new(),
}).collect();
event.alarms = alarms
.into_iter()
.map(|reminder| VAlarm {
action: AlarmAction::Display,
trigger: AlarmTrigger::Duration(chrono::Duration::minutes(
-reminder.minutes_before as i64,
)),
duration: None,
repeat: None,
description: reminder.description,
summary: None,
attendees: Vec::new(),
attach: Vec::new(),
})
.collect();
event.calendar_path = Some(calendar_path.clone());
// Create the event on the CalDAV server
let event_href = client.create_event(&calendar_path, &event)
let event_href = client
.create_event(&calendar_path, &event)
.await
.map_err(|e| ApiError::Internal(format!("Failed to create event: {}", e)))?;
println!("✅ Event created successfully with UID: {} at href: {}", event.uid, event_href);
println!(
"✅ Event created successfully with UID: {} at href: {}",
event.uid, event_href
);
Ok(Json(CreateEventResponse {
success: true,
@@ -505,7 +629,7 @@ pub async fn update_event(
Json(request): Json<UpdateEventRequest>,
) -> Result<Json<UpdateEventResponse>, ApiError> {
// Handle update request
// Extract and verify token
let token = extract_bearer_token(&headers)?;
let password = extract_password_header(&headers)?;
@@ -514,37 +638,45 @@ pub async fn update_event(
if request.uid.trim().is_empty() {
return Err(ApiError::BadRequest("Event UID is required".to_string()));
}
if request.title.trim().is_empty() {
return Err(ApiError::BadRequest("Event title is required".to_string()));
}
if request.title.len() > 200 {
return Err(ApiError::BadRequest("Event title too long (max 200 characters)".to_string()));
return Err(ApiError::BadRequest(
"Event title too long (max 200 characters)".to_string(),
));
}
// Create CalDAV config from token and password
let config = state.auth_service.caldav_config_from_token(&token, &password)?;
let config = state
.auth_service
.caldav_config_from_token(&token, &password)?;
let client = CalDAVClient::new(config);
// Find the event across all calendars (or in the specified calendar)
let calendar_paths = if let Some(path) = &request.calendar_path {
vec![path.clone()]
} else {
client.discover_calendars()
client
.discover_calendars()
.await
.map_err(|e| ApiError::Internal(format!("Failed to discover calendars: {}", e)))?
};
let mut found_event: Option<(CalendarEvent, String, String)> = None; // (event, calendar_path, event_href)
for calendar_path in &calendar_paths {
match client.fetch_events(calendar_path).await {
Ok(events) => {
for event in events {
if event.uid == request.uid {
// Use the actual href from the event, or generate one if missing
let event_href = event.href.clone().unwrap_or_else(|| format!("{}.ics", event.uid));
let event_href = event
.href
.clone()
.unwrap_or_else(|| format!("{}.ics", event.uid));
println!("🔍 Found event {} with href: {}", event.uid, event_href);
found_event = Some((event, calendar_path.clone(), event_href));
break;
@@ -553,9 +685,12 @@ pub async fn update_event(
if found_event.is_some() {
break;
}
},
}
Err(e) => {
eprintln!("Failed to fetch events from calendar {}: {}", calendar_path, e);
eprintln!(
"Failed to fetch events from calendar {}: {}",
calendar_path, e
);
continue;
}
}
@@ -565,23 +700,38 @@ pub async fn update_event(
.ok_or_else(|| ApiError::NotFound(format!("Event with UID '{}' not found", request.uid)))?;
// Parse dates and times
let start_datetime = parse_event_datetime(&request.start_date, &request.start_time, request.all_day)
.map_err(|e| ApiError::BadRequest(format!("Invalid start date/time: {}", e)))?;
let start_datetime =
parse_event_datetime(&request.start_date, &request.start_time, request.all_day)
.map_err(|e| ApiError::BadRequest(format!("Invalid start date/time: {}", e)))?;
let end_datetime = parse_event_datetime(&request.end_date, &request.end_time, request.all_day)
.map_err(|e| ApiError::BadRequest(format!("Invalid end date/time: {}", e)))?;
// Validate that end is after start
if end_datetime <= start_datetime {
return Err(ApiError::BadRequest("End date/time must be after start date/time".to_string()));
return Err(ApiError::BadRequest(
"End date/time must be after start date/time".to_string(),
));
}
// Update event properties
event.dtstart = start_datetime;
event.dtend = Some(end_datetime);
event.summary = if request.title.trim().is_empty() { None } else { Some(request.title) };
event.description = if request.description.trim().is_empty() { None } else { Some(request.description) };
event.location = if request.location.trim().is_empty() { None } else { Some(request.location) };
event.summary = if request.title.trim().is_empty() {
None
} else {
Some(request.title)
};
event.description = if request.description.trim().is_empty() {
None
} else {
Some(request.description)
};
event.location = if request.location.trim().is_empty() {
None
} else {
Some(request.location)
};
event.all_day = request.all_day;
// Parse and update status
@@ -601,11 +751,15 @@ pub async fn update_event(
event.priority = request.priority;
// Update the event on the CalDAV server
println!("📝 Updating event {} at calendar_path: {}, event_href: {}", event.uid, calendar_path, event_href);
client.update_event(&calendar_path, &event, &event_href)
println!(
"📝 Updating event {} at calendar_path: {}, event_href: {}",
event.uid, calendar_path, event_href
);
client
.update_event(&calendar_path, &event, &event_href)
.await
.map_err(|e| ApiError::Internal(format!("Failed to update event: {}", e)))?;
println!("✅ Successfully updated event {}", event.uid);
Ok(Json(UpdateEventResponse {
@@ -614,27 +768,32 @@ pub async fn update_event(
}))
}
fn parse_event_datetime(date_str: &str, time_str: &str, all_day: bool) -> Result<chrono::DateTime<chrono::Utc>, String> {
use chrono::{NaiveDate, NaiveTime, NaiveDateTime, Utc, TimeZone};
fn parse_event_datetime(
date_str: &str,
time_str: &str,
all_day: bool,
) -> Result<chrono::DateTime<chrono::Utc>, String> {
use chrono::{NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc};
// Parse the date
let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d")
.map_err(|_| format!("Invalid date format: {}. Expected YYYY-MM-DD", date_str))?;
if all_day {
// For all-day events, use midnight UTC
let datetime = date.and_hms_opt(0, 0, 0)
let datetime = date
.and_hms_opt(0, 0, 0)
.ok_or_else(|| "Failed to create midnight datetime".to_string())?;
Ok(Utc.from_utc_datetime(&datetime))
} else {
// Parse the time
let time = NaiveTime::parse_from_str(time_str, "%H:%M")
.map_err(|_| format!("Invalid time format: {}. Expected HH:MM", time_str))?;
// Combine date and time
let datetime = NaiveDateTime::new(date, time);
// Assume local time and convert to UTC (in a real app, you'd want timezone support)
Ok(Utc.from_utc_datetime(&datetime))
}
}
}

File diff suppressed because it is too large Load Diff