use axum::{ extract::{State, Query, Path}, http::HeaderMap, response::Json, }; use serde::Deserialize; use std::sync::Arc; use chrono::Datelike; use crate::{AppState, models::{CalDAVLoginRequest, AuthResponse, ApiError, UserInfo, CalendarInfo, CreateCalendarRequest, CreateCalendarResponse, DeleteCalendarRequest, DeleteCalendarResponse, DeleteEventRequest, DeleteEventResponse, CreateEventRequest, CreateEventResponse}}; use crate::calendar::{CalDAVClient, CalendarEvent}; #[derive(Deserialize)] pub struct CalendarQuery { pub year: Option, pub month: Option, } pub async fn get_calendar_events( State(state): State>, Query(params): Query, headers: HeaderMap, ) -> Result>, ApiError> { // Extract and verify token 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 client = CalDAVClient::new(config); // Discover calendars if needed 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 { match client.fetch_events(calendar_path).await { Ok(mut events) => { // Set calendar_path for each event to identify which calendar it belongs to for event in &mut events { event.calendar_path = Some(calendar_path.clone()); } all_events.extend(events); }, Err(e) => { // Log the error but continue with other calendars eprintln!("Failed to fetch events from calendar {}: {}", calendar_path, e); } } } let events = all_events; // Filter events by month if specified let filtered_events = if let (Some(year), Some(month)) = (params.year, params.month) { events.into_iter().filter(|event| { let event_date = event.start.date_naive(); event_date.year() == year && event_date.month() == month }).collect() } else { events }; Ok(Json(filtered_events)) } pub async fn refresh_event( State(state): State>, Path(uid): Path, headers: HeaderMap, ) -> Result>, ApiError> { // Extract and verify token 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 client = CalDAVClient::new(config); // Discover calendars if needed 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(None)); // No calendars found } // Search for the specific event by UID across all calendars let mut found_event = None; for calendar_path in &calendar_paths { match client.fetch_event_by_uid(calendar_path, &uid).await { Ok(Some(mut event)) => { event.calendar_path = Some(calendar_path.clone()); found_event = Some(event); break; }, Ok(None) => continue, // Event not found in this calendar Err(e) => { eprintln!("Failed to fetch event from calendar {}: {}", calendar_path, e); continue; } } } let event = found_event; Ok(Json(event)) } pub async fn login( State(state): State>, Json(request): Json, ) -> Result, ApiError> { println!("🔐 Login attempt:"); println!(" Server URL: {}", request.server_url); println!(" Username: {}", request.username); println!(" Password length: {}", request.password.len()); let response = state.auth_service.login(request).await?; Ok(Json(response)) } pub async fn verify_token( State(state): State>, headers: HeaderMap, ) -> Result, ApiError> { let token = extract_bearer_token(&headers)?; let claims = state.auth_service.verify_token(&token)?; Ok(Json(serde_json::json!({ "valid": true, "username": claims.username, "server_url": claims.server_url }))) } pub async fn get_user_info( State(state): State>, headers: HeaderMap, ) -> Result, ApiError> { // Extract and verify token let token = extract_bearer_token(&headers)?; let password = extract_password_header(&headers)?; let claims = state.auth_service.verify_token(&token)?; // Create CalDAV config from token and 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() .await .map_err(|e| ApiError::Internal(format!("Failed to discover calendars: {}", e)))?; // Convert paths to CalendarInfo structs with display names, filtering out generic collections let calendars: Vec = calendar_paths.into_iter() .filter_map(|path| { let display_name = extract_calendar_name(&path); // Skip generic collection names if display_name.eq_ignore_ascii_case("calendar") || display_name.eq_ignore_ascii_case("calendars") || display_name.eq_ignore_ascii_case("collection") { None } else { Some(CalendarInfo { path: path.clone(), display_name, color: generate_calendar_color(&path), }) } }).collect(); Ok(Json(UserInfo { username: claims.username, server_url: claims.server_url, calendars, })) } // Helper function to generate a consistent color for a calendar based on its path fn generate_calendar_color(path: &str) -> String { // Predefined set of attractive, accessible colors for calendars let colors = [ "#3B82F6", // Blue "#10B981", // Emerald "#F59E0B", // Amber "#EF4444", // Red "#8B5CF6", // Violet "#06B6D4", // Cyan "#84CC16", // Lime "#F97316", // Orange "#EC4899", // Pink "#6366F1", // Indigo "#14B8A6", // Teal "#F3B806", // Yellow "#8B5A2B", // Brown "#6B7280", // Gray "#DC2626", // Red-600 "#7C3AED", // Violet-600 ]; // Create a simple hash from the path to ensure consistent color assignment let mut hash: u32 = 0; for byte in path.bytes() { hash = hash.wrapping_mul(31).wrapping_add(byte as u32); } // Use the hash to select a color from our palette let color_index = (hash as usize) % colors.len(); colors[color_index].to_string() } // Helper function to extract a readable calendar name from path fn extract_calendar_name(path: &str) -> String { // Extract the last meaningful part of the path // e.g., "/calendars/user/personal/" -> "personal" // or "/calendars/user/work-calendar/" -> "work-calendar" let parts: Vec<&str> = path.trim_end_matches('/').split('/').collect(); if let Some(last_part) = parts.last() { if !last_part.is_empty() && *last_part != "calendars" { // Convert kebab-case or snake_case to title case last_part .replace('-', " ") .replace('_', " ") .split_whitespace() .map(|word| { let mut chars = word.chars(); match chars.next() { None => String::new(), Some(first) => first.to_uppercase().collect::() + chars.as_str(), } }) .collect::>() .join(" ") } else if parts.len() > 1 { // If the last part is empty or "calendars", try the second to last extract_calendar_name(&parts[..parts.len()-1].join("/")) } else { "Calendar".to_string() } } else { "Calendar".to_string() } } // Helper functions fn extract_bearer_token(headers: &HeaderMap) -> Result { if let Some(auth_header) = headers.get("authorization") { let auth_str = auth_header .to_str() .map_err(|_| ApiError::Unauthorized("Invalid authorization header".to_string()))?; if let Some(token) = auth_str.strip_prefix("Bearer ") { Ok(token.to_string()) } else { Err(ApiError::Unauthorized("Authorization header must start with 'Bearer '".to_string())) } } else { Err(ApiError::Unauthorized("Authorization header required".to_string())) } } fn extract_password_header(headers: &HeaderMap) -> Result { if let Some(password_header) = headers.get("x-caldav-password") { let password = password_header .to_str() .map_err(|_| ApiError::BadRequest("Invalid password header".to_string()))?; Ok(password.to_string()) } else { Err(ApiError::BadRequest("X-CalDAV-Password header required".to_string())) } } pub async fn create_calendar( State(state): State>, headers: HeaderMap, Json(request): Json, ) -> Result, ApiError> { println!("📝 Create calendar request received: name='{}', description={:?}, color={:?}", request.name, request.description, request.color); // Extract and verify token let token = extract_bearer_token(&headers)?; let password = extract_password_header(&headers)?; // Validate request if request.name.trim().is_empty() { return Err(ApiError::BadRequest("Calendar name is required".to_string())); } if request.name.len() > 100 { return Err(ApiError::BadRequest("Calendar name too long (max 100 characters)".to_string())); } // Create CalDAV config from token and password let config = state.auth_service.caldav_config_from_token(&token, &password)?; let client = CalDAVClient::new(config); // Create the calendar client.create_calendar( &request.name, request.description.as_deref(), request.color.as_deref() ) .await .map_err(|e| ApiError::Internal(format!("Failed to create calendar: {}", e)))?; Ok(Json(CreateCalendarResponse { success: true, message: "Calendar created successfully".to_string(), })) } pub async fn delete_calendar( State(state): State>, headers: HeaderMap, Json(request): Json, ) -> Result, ApiError> { println!("🗑️ Delete calendar request received: path='{}'", request.path); // Extract and verify token let token = extract_bearer_token(&headers)?; let password = extract_password_header(&headers)?; // Validate request if request.path.trim().is_empty() { 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 client = CalDAVClient::new(config); // Delete the calendar client.delete_calendar(&request.path) .await .map_err(|e| ApiError::Internal(format!("Failed to delete calendar: {}", e)))?; Ok(Json(DeleteCalendarResponse { success: true, message: "Calendar deleted successfully".to_string(), })) } pub async fn delete_event( State(state): State>, headers: HeaderMap, Json(request): Json, ) -> Result, ApiError> { println!("🗑️ Delete event request received: calendar_path='{}', event_href='{}'", request.calendar_path, request.event_href); // Extract and verify token let token = extract_bearer_token(&headers)?; let password = extract_password_header(&headers)?; // Validate request if request.calendar_path.trim().is_empty() { return Err(ApiError::BadRequest("Calendar path is required".to_string())); } if request.event_href.trim().is_empty() { return Err(ApiError::BadRequest("Event href is required".to_string())); } // Create CalDAV config from token and password let config = state.auth_service.caldav_config_from_token(&token, &password)?; let client = CalDAVClient::new(config); // Delete the event 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(), })) } pub async fn create_event( State(state): State>, headers: HeaderMap, Json(request): Json, ) -> Result, ApiError> { 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)?; // Validate request 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())); } // Create CalDAV config from token and password let config = state.auth_service.caldav_config_from_token(&token, &password)?; let client = CalDAVClient::new(config); // Determine which calendar to use let calendar_path = if let Some(path) = request.calendar_path { path } else { // Use the first available calendar 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())); } 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 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())); } // Generate a unique UID for the event let uid = format!("{}-{}", uuid::Uuid::new_v4(), chrono::Utc::now().timestamp()); // Parse status let status = match request.status.to_lowercase().as_str() { "tentative" => crate::calendar::EventStatus::Tentative, "cancelled" => crate::calendar::EventStatus::Cancelled, _ => crate::calendar::EventStatus::Confirmed, }; // Parse class let class = match request.class.to_lowercase().as_str() { "private" => crate::calendar::EventClass::Private, "confidential" => crate::calendar::EventClass::Confidential, _ => crate::calendar::EventClass::Public, }; // Parse attendees (comma-separated email list) let attendees: Vec = if request.attendees.trim().is_empty() { Vec::new() } else { request.attendees .split(',') .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) .collect() }; // Parse categories (comma-separated list) let categories: Vec = if request.categories.trim().is_empty() { Vec::new() } else { request.categories .split(',') .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) .collect() }; // Parse reminders and convert to EventReminder structs let reminders: Vec = match request.reminder.to_lowercase().as_str() { "15min" => vec![crate::calendar::EventReminder { minutes_before: 15, action: crate::calendar::ReminderAction::Display, description: None, }], "30min" => vec![crate::calendar::EventReminder { minutes_before: 30, action: crate::calendar::ReminderAction::Display, description: None, }], "1hour" => vec![crate::calendar::EventReminder { minutes_before: 60, action: crate::calendar::ReminderAction::Display, description: None, }], "2hours" => vec![crate::calendar::EventReminder { minutes_before: 120, action: crate::calendar::ReminderAction::Display, description: None, }], "1day" => vec![crate::calendar::EventReminder { minutes_before: 1440, // 24 * 60 action: crate::calendar::ReminderAction::Display, description: None, }], "2days" => vec![crate::calendar::EventReminder { minutes_before: 2880, // 48 * 60 action: crate::calendar::ReminderAction::Display, description: None, }], "1week" => vec![crate::calendar::EventReminder { minutes_before: 10080, // 7 * 24 * 60 action: crate::calendar::ReminderAction::Display, description: None, }], _ => Vec::new(), }; // Parse recurrence with BYDAY support for weekly recurrence let recurrence_rule = match request.recurrence.to_lowercase().as_str() { "daily" => Some("FREQ=DAILY".to_string()), "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 .iter() .enumerate() .filter_map(|(i, &selected)| { if selected { Some(match i { 0 => "SU", // Sunday 1 => "MO", // Monday 2 => "TU", // Tuesday 3 => "WE", // Wednesday 4 => "TH", // Thursday 5 => "FR", // Friday 6 => "SA", // Saturday _ => return None, }) } else { None } }) .collect(); if !selected_days.is_empty() { rrule.push_str(&format!(";BYDAY={}", selected_days.join(","))); } } Some(rrule) }, "monthly" => Some("FREQ=MONTHLY".to_string()), "yearly" => Some("FREQ=YEARLY".to_string()), _ => None, }; // Create the CalendarEvent struct let event = crate::calendar::CalendarEvent { uid, summary: Some(request.title.clone()), description: if request.description.trim().is_empty() { None } else { Some(request.description.clone()) }, start: start_datetime, end: Some(end_datetime), location: if request.location.trim().is_empty() { None } else { Some(request.location.clone()) }, status, class, priority: request.priority, organizer: if request.organizer.trim().is_empty() { None } else { Some(request.organizer.clone()) }, attendees, categories, created: Some(chrono::Utc::now()), last_modified: Some(chrono::Utc::now()), recurrence_rule, all_day: request.all_day, reminders, etag: None, href: None, calendar_path: Some(calendar_path.clone()), }; // Create the event on the CalDAV server let event_href = client.create_event(&calendar_path, &event) .await .map_err(|e| ApiError::Internal(format!("Failed to create event: {}", e)))?; Ok(Json(CreateEventResponse { success: true, message: "Event created successfully".to_string(), event_href: Some(event_href), })) } /// Parse date and time strings into a UTC DateTime fn parse_event_datetime(date_str: &str, time_str: &str, all_day: bool) -> Result, String> { use chrono::{NaiveDate, NaiveTime, NaiveDateTime, Utc, TimeZone}; // 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) .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)) } }