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>
157 lines
5.3 KiB
Rust
157 lines
5.3 KiB
Rust
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;
|
|
|
|
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());
|
|
|
|
// 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()));
|
|
}
|
|
|
|
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()
|
|
);
|
|
let client = CalDAVClient::new(config);
|
|
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)?;
|
|
|
|
println!("🔗 Created CalDAV client, attempting to discover calendars...");
|
|
|
|
Ok(Json(AuthResponse {
|
|
token,
|
|
username: request.username,
|
|
server_url: request.server_url,
|
|
}))
|
|
}
|
|
|
|
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),
|
|
}
|
|
}).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(" ")
|
|
} |