Users can now toggle visibility of CalDAV calendars using checkboxes in the sidebar, matching the behavior of external calendars. Events from hidden calendars are automatically filtered out of the calendar view. Changes: - Add is_visible field to CalendarInfo (frontend & backend) - Add visibility checkboxes to CalDAV calendar list items - Implement real-time event filtering based on calendar visibility - Add CSS styling matching external calendar checkboxes - Default new calendars to visible state 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
145 lines
4.5 KiB
Rust
145 lines
4.5 KiB
Rust
use axum::{extract::State, http::HeaderMap, response::Json};
|
|
use std::sync::Arc;
|
|
|
|
use crate::calendar::CalDAVClient;
|
|
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")
|
|
.ok_or_else(|| ApiError::Unauthorized("Missing Authorization header".to_string()))?;
|
|
|
|
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(),
|
|
))
|
|
}
|
|
}
|
|
|
|
pub fn extract_password_header(headers: &HeaderMap) -> Result<String, ApiError> {
|
|
let password_header = headers
|
|
.get("x-caldav-password")
|
|
.ok_or_else(|| ApiError::BadRequest("Missing X-CalDAV-Password header".to_string()))?;
|
|
|
|
password_header
|
|
.to_str()
|
|
.map(|s| s.to_string())
|
|
.map_err(|_| ApiError::BadRequest("Invalid X-CalDAV-Password header".to_string()))
|
|
}
|
|
|
|
pub async fn login(
|
|
State(state): State<Arc<AppState>>,
|
|
Json(request): Json<CalDAVLoginRequest>,
|
|
) -> Result<Json<AuthResponse>, ApiError> {
|
|
println!("🔐 Login attempt:");
|
|
println!(" Server URL: {}", request.server_url);
|
|
println!(" Username: {}", request.username);
|
|
println!(" Password length: {}", request.password.len());
|
|
|
|
// Use the auth service login method which now handles database, sessions, and preferences
|
|
let response = state.auth_service.login(request).await?;
|
|
|
|
println!("✅ Login successful with session management");
|
|
|
|
Ok(Json(response))
|
|
}
|
|
|
|
pub async fn verify_token(
|
|
State(state): State<Arc<AppState>>,
|
|
headers: HeaderMap,
|
|
) -> 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 })))
|
|
}
|
|
|
|
pub async fn get_user_info(
|
|
State(state): State<Arc<AppState>>,
|
|
headers: HeaderMap,
|
|
) -> 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 client = CalDAVClient::new(config.clone());
|
|
|
|
// 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 {
|
|
path: path.clone(),
|
|
display_name: extract_calendar_name(path),
|
|
color: generate_calendar_color(path),
|
|
is_visible: true, // Default to visible
|
|
})
|
|
.collect();
|
|
|
|
Ok(Json(UserInfo {
|
|
username: config.username,
|
|
server_url: config.server_url,
|
|
calendars,
|
|
}))
|
|
}
|
|
|
|
fn generate_calendar_color(path: &str) -> String {
|
|
// Generate a consistent color based on the calendar path
|
|
// This is a simple hash-based approach
|
|
let mut hash: u32 = 0;
|
|
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",
|
|
];
|
|
|
|
colors[(hash as usize) % colors.len()].to_string()
|
|
}
|
|
|
|
fn extract_calendar_name(path: &str) -> String {
|
|
// Extract calendar name from path
|
|
// E.g., "/calendars/user/calendar-name/" -> "Calendar Name"
|
|
path.split('/')
|
|
.filter(|s| !s.is_empty())
|
|
.last()
|
|
.unwrap_or("Calendar")
|
|
.replace('-', " ")
|
|
.replace('_', " ")
|
|
.split_whitespace()
|
|
.map(|word| {
|
|
let mut chars = word.chars();
|
|
match chars.next() {
|
|
None => String::new(),
|
|
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
|
|
}
|
|
})
|
|
.collect::<Vec<String>>()
|
|
.join(" ")
|
|
}
|