Refactor authentication from database to direct CalDAV authentication

Major architectural change to simplify authentication by authenticating directly against CalDAV servers instead of maintaining a local user database.

Backend changes:
- Remove SQLite database dependencies and user storage
- Refactor AuthService to authenticate directly against CalDAV servers
- Update JWT tokens to store CalDAV server info instead of user IDs
- Implement proper CalDAV calendar discovery with XML parsing
- Fix URL construction for CalDAV REPORT requests
- Add comprehensive debug logging for authentication flow

Frontend changes:
- Add server URL input field to login form
- Remove registration functionality entirely
- Update calendar service to pass CalDAV passwords via headers
- Store CalDAV credentials in localStorage for API calls

Key improvements:
- Simplified architecture eliminates database complexity
- Direct CalDAV authentication ensures credentials always work
- Proper calendar discovery automatically finds user calendars
- Robust error handling and debug logging for troubleshooting

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Connor Johnstone
2025-08-28 18:40:22 -04:00
parent 0741afd0b2
commit d85898cae7
12 changed files with 276 additions and 582 deletions

View File

@@ -7,9 +7,8 @@ use serde::Deserialize;
use std::sync::Arc;
use chrono::Datelike;
use crate::{AppState, models::{LoginRequest, RegisterRequest, AuthResponse, ApiError}};
use crate::{AppState, models::{CalDAVLoginRequest, AuthResponse, ApiError}};
use crate::calendar::{CalDAVClient, CalendarEvent};
use crate::config::CalDAVConfig;
#[derive(Deserialize)]
pub struct CalendarQuery {
@@ -18,31 +17,17 @@ pub struct CalendarQuery {
}
pub async fn get_calendar_events(
State(_state): State<Arc<AppState>>,
State(state): State<Arc<AppState>>,
Query(params): Query<CalendarQuery>,
headers: HeaderMap,
) -> Result<Json<Vec<CalendarEvent>>, ApiError> {
// Verify authentication (extract token from Authorization header)
let _token = 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 auth_str.starts_with("Bearer ") {
auth_str.strip_prefix("Bearer ").unwrap().to_string()
} else {
return Err(ApiError::Unauthorized("Invalid authorization format".to_string()));
}
} else {
return Err(ApiError::Unauthorized("Missing authorization header".to_string()));
};
// TODO: Validate JWT token here
// Load CalDAV configuration
let config = CalDAVConfig::from_env()
.map_err(|e| ApiError::Internal(format!("Failed to load CalDAV config: {}", e)))?;
// 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
@@ -74,31 +59,16 @@ pub async fn get_calendar_events(
}
pub async fn refresh_event(
State(_state): State<Arc<AppState>>,
State(state): State<Arc<AppState>>,
Path(uid): Path<String>,
headers: HeaderMap,
) -> Result<Json<Option<CalendarEvent>>, ApiError> {
// Verify authentication (extract token from Authorization header)
let _token = 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 auth_str.starts_with("Bearer ") {
auth_str.strip_prefix("Bearer ").unwrap().to_string()
} else {
return Err(ApiError::Unauthorized("Invalid authorization format".to_string()));
}
} else {
return Err(ApiError::Unauthorized("Missing authorization header".to_string()));
};
// TODO: Validate JWT token here
// Load CalDAV configuration
let config = CalDAVConfig::from_env()
.map_err(|e| ApiError::Internal(format!("Failed to load CalDAV config: {}", e)))?;
// 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
@@ -119,18 +89,15 @@ pub async fn refresh_event(
Ok(Json(event))
}
pub async fn register(
State(state): State<Arc<AppState>>,
Json(request): Json<RegisterRequest>,
) -> Result<Json<AuthResponse>, ApiError> {
let response = state.auth_service.register(request).await?;
Ok(Json(response))
}
pub async fn login(
State(state): State<Arc<AppState>>,
Json(request): Json<LoginRequest>,
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());
let response = state.auth_service.login(request).await?;
Ok(Json(response))
}
@@ -139,25 +106,40 @@ pub async fn verify_token(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
) -> Result<Json<serde_json::Value>, ApiError> {
// Try to get token from Authorization header
let token = if let Some(auth_header) = headers.get("authorization") {
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 ") {
token.to_string()
} else {
return Err(ApiError::BadRequest("Authorization header must start with 'Bearer '".to_string()));
}
} else {
return Err(ApiError::Unauthorized("Authorization header required".to_string()));
};
let user_info = state.auth_service.verify_token(&token).await?;
let token = extract_bearer_token(&headers)?;
let claims = state.auth_service.verify_token(&token)?;
Ok(Json(serde_json::json!({
"valid": true,
"user": user_info
"username": claims.username,
"server_url": claims.server_url
})))
}
// Helper functions
fn extract_bearer_token(headers: &HeaderMap) -> Result<String, ApiError> {
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<String, ApiError> {
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()))
}
}