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:
		| @@ -9,8 +9,6 @@ path = "src/main.rs" | ||||
|  | ||||
| [dependencies] | ||||
| # Backend authentication dependencies | ||||
| sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "sqlite", "chrono", "uuid", "migrate"] } | ||||
| bcrypt = "0.15" | ||||
| jsonwebtoken = "9.0" | ||||
| tokio = { version = "1.0", features = ["full"] } | ||||
| axum = { version = "0.7", features = ["json"] } | ||||
|   | ||||
| @@ -1,210 +1,118 @@ | ||||
| use bcrypt::{hash, verify, DEFAULT_COST}; | ||||
| use chrono::{Duration, Utc}; | ||||
| use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation}; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use sqlx::{Row, SqlitePool}; | ||||
| use uuid::Uuid; | ||||
|  | ||||
| use crate::models::{User, UserInfo, LoginRequest, RegisterRequest, AuthResponse, ApiError}; | ||||
| use crate::models::{CalDAVLoginRequest, AuthResponse, ApiError}; | ||||
| use crate::config::CalDAVConfig; | ||||
| use crate::calendar::CalDAVClient; | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize)] | ||||
| pub struct Claims { | ||||
|     pub sub: String,  // Subject (user ID) | ||||
|     pub username: String, | ||||
|     pub server_url: String, | ||||
|     pub exp: i64,     // Expiration time | ||||
|     pub iat: i64,     // Issued at | ||||
|     pub username: String, | ||||
|     pub email: String, | ||||
| } | ||||
|  | ||||
| #[derive(Clone)] | ||||
| pub struct AuthService { | ||||
|     db: SqlitePool, | ||||
|     jwt_secret: String, | ||||
| } | ||||
|  | ||||
| impl AuthService { | ||||
|     pub fn new(db: SqlitePool, jwt_secret: String) -> Self { | ||||
|         Self { db, jwt_secret } | ||||
|     pub fn new(jwt_secret: String) -> Self { | ||||
|         Self { jwt_secret } | ||||
|     } | ||||
|  | ||||
|     pub async fn init_db(&self) -> Result<(), ApiError> { | ||||
|         sqlx::query( | ||||
|             r#" | ||||
|             CREATE TABLE IF NOT EXISTS users ( | ||||
|                 id TEXT PRIMARY KEY, | ||||
|                 username TEXT UNIQUE NOT NULL, | ||||
|                 email TEXT UNIQUE NOT NULL, | ||||
|                 password_hash TEXT NOT NULL, | ||||
|                 created_at DATETIME DEFAULT CURRENT_TIMESTAMP, | ||||
|                 updated_at DATETIME DEFAULT CURRENT_TIMESTAMP | ||||
|             ) | ||||
|             "#, | ||||
|         ) | ||||
|         .execute(&self.db) | ||||
|         .await | ||||
|         .map_err(|e| ApiError::Database(e.to_string()))?; | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     pub async fn register(&self, request: RegisterRequest) -> Result<AuthResponse, ApiError> { | ||||
|     /// Authenticate user directly against CalDAV server | ||||
|     pub async fn login(&self, request: CalDAVLoginRequest) -> Result<AuthResponse, ApiError> { | ||||
|         // Validate input | ||||
|         self.validate_registration(&request)?; | ||||
|         self.validate_login(&request)?; | ||||
|         println!("✅ Input validation passed"); | ||||
|  | ||||
|         // Check if user already exists | ||||
|         if self.user_exists(&request.username, &request.email).await? { | ||||
|             return Err(ApiError::Conflict("Username or email already exists".to_string())); | ||||
|         } | ||||
|         // Create CalDAV config with provided credentials | ||||
|         let caldav_config = CalDAVConfig { | ||||
|             server_url: request.server_url.clone(), | ||||
|             username: request.username.clone(), | ||||
|             password: request.password.clone(), | ||||
|             calendar_path: None, | ||||
|             tasks_path: None, | ||||
|         }; | ||||
|         println!("📝 Created CalDAV config"); | ||||
|  | ||||
|         // Hash password | ||||
|         let password_hash = hash(&request.password, DEFAULT_COST) | ||||
|             .map_err(|e| ApiError::Internal(format!("Password hashing failed: {}", e)))?; | ||||
|  | ||||
|         // Create user | ||||
|         let user_id = Uuid::new_v4().to_string(); | ||||
|         let now = Utc::now(); | ||||
|  | ||||
|         sqlx::query( | ||||
|             r#" | ||||
|             INSERT INTO users (id, username, email, password_hash, created_at, updated_at) | ||||
|             VALUES (?, ?, ?, ?, ?, ?) | ||||
|             "#, | ||||
|         ) | ||||
|         .bind(&user_id) | ||||
|         .bind(&request.username) | ||||
|         .bind(&request.email) | ||||
|         .bind(&password_hash) | ||||
|         .bind(now) | ||||
|         .bind(now) | ||||
|         .execute(&self.db) | ||||
|         .await | ||||
|         .map_err(|e| ApiError::Database(e.to_string()))?; | ||||
|  | ||||
|         // Get the created user | ||||
|         let user = self.get_user_by_id(&user_id).await?; | ||||
|  | ||||
|         // Generate token | ||||
|         let token = self.generate_token(&user)?; | ||||
|  | ||||
|         Ok(AuthResponse { | ||||
|             token, | ||||
|             user: UserInfo { | ||||
|                 id: user.id, | ||||
|                 username: user.username, | ||||
|                 email: user.email, | ||||
|             }, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     pub async fn login(&self, request: LoginRequest) -> Result<AuthResponse, ApiError> { | ||||
|         // Get user by username | ||||
|         let user = self.get_user_by_username(&request.username).await?; | ||||
|  | ||||
|         // Verify password | ||||
|         let is_valid = verify(&request.password, &user.password_hash) | ||||
|             .map_err(|e| ApiError::Internal(format!("Password verification failed: {}", e)))?; | ||||
|  | ||||
|         if !is_valid { | ||||
|             return Err(ApiError::Unauthorized("Invalid credentials".to_string())); | ||||
|         } | ||||
|  | ||||
|         // Generate token | ||||
|         let token = self.generate_token(&user)?; | ||||
|  | ||||
|         Ok(AuthResponse { | ||||
|             token, | ||||
|             user: UserInfo { | ||||
|                 id: user.id, | ||||
|                 username: user.username, | ||||
|                 email: user.email, | ||||
|             }, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     pub async fn verify_token(&self, token: &str) -> Result<UserInfo, ApiError> { | ||||
|         let claims = self.decode_token(token)?; | ||||
|         let user = self.get_user_by_id(&claims.sub).await?; | ||||
|         // Test authentication against CalDAV server | ||||
|         let caldav_client = CalDAVClient::new(caldav_config.clone()); | ||||
|         println!("🔗 Created CalDAV client, attempting to discover calendars..."); | ||||
|          | ||||
|         Ok(UserInfo { | ||||
|             id: user.id, | ||||
|             username: user.username, | ||||
|             email: user.email, | ||||
|         // Try to discover calendars as an authentication test | ||||
|         match caldav_client.discover_calendars().await { | ||||
|             Ok(calendars) => { | ||||
|                 println!("✅ Authentication successful! Found {} calendars", calendars.len()); | ||||
|                 // Authentication successful, generate JWT token | ||||
|                 let token = self.generate_token(&request.username, &request.server_url)?; | ||||
|                  | ||||
|                 Ok(AuthResponse { | ||||
|                     token, | ||||
|                     username: request.username, | ||||
|                     server_url: request.server_url, | ||||
|                 }) | ||||
|             } | ||||
|             Err(err) => { | ||||
|                 println!("❌ Authentication failed: {:?}", err); | ||||
|                 // Authentication failed | ||||
|                 Err(ApiError::Unauthorized("Invalid CalDAV credentials or server unavailable".to_string())) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Verify JWT token and extract CalDAV credentials info | ||||
|     pub fn verify_token(&self, token: &str) -> Result<Claims, ApiError> { | ||||
|         self.decode_token(token) | ||||
|     } | ||||
|  | ||||
|     /// Create CalDAV config from token | ||||
|     pub fn caldav_config_from_token(&self, token: &str, password: &str) -> Result<CalDAVConfig, ApiError> { | ||||
|         let claims = self.verify_token(token)?; | ||||
|          | ||||
|         Ok(CalDAVConfig { | ||||
|             server_url: claims.server_url, | ||||
|             username: claims.username, | ||||
|             password: password.to_string(), | ||||
|             calendar_path: None, | ||||
|             tasks_path: None, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     async fn get_user_by_username(&self, username: &str) -> Result<User, ApiError> { | ||||
|         let row = sqlx::query("SELECT id, username, email, password_hash, created_at FROM users WHERE username = ?") | ||||
|             .bind(username) | ||||
|             .fetch_one(&self.db) | ||||
|             .await | ||||
|             .map_err(|_| ApiError::Unauthorized("Invalid credentials".to_string()))?; | ||||
|  | ||||
|         self.row_to_user(row) | ||||
|     } | ||||
|  | ||||
|     async fn get_user_by_id(&self, user_id: &str) -> Result<User, ApiError> { | ||||
|         let row = sqlx::query("SELECT id, username, email, password_hash, created_at FROM users WHERE id = ?") | ||||
|             .bind(user_id) | ||||
|             .fetch_one(&self.db) | ||||
|             .await | ||||
|             .map_err(|_| ApiError::NotFound("User not found".to_string()))?; | ||||
|  | ||||
|         self.row_to_user(row) | ||||
|     } | ||||
|  | ||||
|     fn row_to_user(&self, row: sqlx::sqlite::SqliteRow) -> Result<User, ApiError> { | ||||
|         Ok(User { | ||||
|             id: row.try_get("id").map_err(|e| ApiError::Database(e.to_string()))?, | ||||
|             username: row.try_get("username").map_err(|e| ApiError::Database(e.to_string()))?, | ||||
|             email: row.try_get("email").map_err(|e| ApiError::Database(e.to_string()))?, | ||||
|             password_hash: row.try_get("password_hash").map_err(|e| ApiError::Database(e.to_string()))?, | ||||
|             created_at: row.try_get("created_at").map_err(|e| ApiError::Database(e.to_string()))?, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     async fn user_exists(&self, username: &str, email: &str) -> Result<bool, ApiError> { | ||||
|         let count: i64 = sqlx::query_scalar( | ||||
|             "SELECT COUNT(*) FROM users WHERE username = ? OR email = ?" | ||||
|         ) | ||||
|         .bind(username) | ||||
|         .bind(email) | ||||
|         .fetch_one(&self.db) | ||||
|         .await | ||||
|         .map_err(|e| ApiError::Database(e.to_string()))?; | ||||
|  | ||||
|         Ok(count > 0) | ||||
|     } | ||||
|  | ||||
|     fn validate_registration(&self, request: &RegisterRequest) -> Result<(), ApiError> { | ||||
|     fn validate_login(&self, request: &CalDAVLoginRequest) -> Result<(), ApiError> { | ||||
|         if request.username.trim().is_empty() { | ||||
|             return Err(ApiError::BadRequest("Username is required".to_string())); | ||||
|         } | ||||
|  | ||||
|         if request.username.len() < 3 { | ||||
|             return Err(ApiError::BadRequest("Username must be at least 3 characters".to_string())); | ||||
|         if request.password.trim().is_empty() { | ||||
|             return Err(ApiError::BadRequest("Password is required".to_string())); | ||||
|         } | ||||
|  | ||||
|         if request.email.trim().is_empty() || !request.email.contains('@') { | ||||
|             return Err(ApiError::BadRequest("Valid email is required".to_string())); | ||||
|         if request.server_url.trim().is_empty() { | ||||
|             return Err(ApiError::BadRequest("Server URL is required".to_string())); | ||||
|         } | ||||
|  | ||||
|         if request.password.len() < 6 { | ||||
|             return Err(ApiError::BadRequest("Password must be at least 6 characters".to_string())); | ||||
|         // Basic URL validation | ||||
|         if !request.server_url.starts_with("http://") && !request.server_url.starts_with("https://") { | ||||
|             return Err(ApiError::BadRequest("Server URL must start with http:// or https://".to_string())); | ||||
|         } | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     fn generate_token(&self, user: &User) -> Result<String, ApiError> { | ||||
|     fn generate_token(&self, username: &str, server_url: &str) -> Result<String, ApiError> { | ||||
|         let now = Utc::now(); | ||||
|         let expires_at = now + Duration::hours(24); // Token valid for 24 hours | ||||
|  | ||||
|         let claims = Claims { | ||||
|             sub: user.id.clone(), | ||||
|             username: username.to_string(), | ||||
|             server_url: server_url.to_string(), | ||||
|             exp: expires_at.timestamp(), | ||||
|             iat: now.timestamp(), | ||||
|             username: user.username.clone(), | ||||
|             email: user.email.clone(), | ||||
|         }; | ||||
|  | ||||
|         let token = encode( | ||||
|   | ||||
| @@ -149,12 +149,26 @@ impl CalDAVClient { | ||||
|         let url = if calendar_path.starts_with("http") { | ||||
|             calendar_path.to_string() | ||||
|         } else { | ||||
|             format!("{}{}", self.config.server_url.trim_end_matches('/'), calendar_path) | ||||
|             // Extract the base URL (scheme + host + port) from server_url | ||||
|             let server_url = &self.config.server_url; | ||||
|             // Find the first '/' after "https://" or "http://" | ||||
|             let scheme_end = if server_url.starts_with("https://") { 8 } else { 7 }; | ||||
|             if let Some(path_start) = server_url[scheme_end..].find('/') { | ||||
|                 let base_url = &server_url[..scheme_end + path_start]; | ||||
|                 format!("{}{}", base_url, calendar_path) | ||||
|             } else { | ||||
|                 // No path in server_url, so just append the calendar_path | ||||
|                 format!("{}{}", server_url.trim_end_matches('/'), calendar_path) | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         let basic_auth = self.config.get_basic_auth(); | ||||
|         println!("🔑 REPORT Basic Auth: Basic {}", basic_auth); | ||||
|         println!("🌐 REPORT URL: {}", url); | ||||
|          | ||||
|         let response = self.http_client | ||||
|             .request(reqwest::Method::from_bytes(b"REPORT").unwrap(), &url) | ||||
|             .header("Authorization", format!("Basic {}", self.config.get_basic_auth())) | ||||
|             .header("Authorization", format!("Basic {}", basic_auth)) | ||||
|             .header("Content-Type", "application/xml") | ||||
|             .header("Depth", "1") | ||||
|             .header("User-Agent", "calendar-app/0.1.0") | ||||
| @@ -296,7 +310,7 @@ impl CalDAVClient { | ||||
|         // Parse end time (optional - use start time if not present) | ||||
|         let end = if let Some(dtend) = properties.get("DTEND") { | ||||
|             Some(self.parse_datetime(dtend, properties.get("DTEND"))?) | ||||
|         } else if let Some(duration) = properties.get("DURATION") { | ||||
|         } else if let Some(_duration) = properties.get("DURATION") { | ||||
|             // TODO: Parse duration and add to start time | ||||
|             Some(start) | ||||
|         } else { | ||||
| @@ -443,18 +457,16 @@ impl CalDAVClient { | ||||
|             println!("Using configured calendar path: {}", calendar_path); | ||||
|             return Ok(vec![calendar_path.clone()]); | ||||
|         } | ||||
|  | ||||
|          | ||||
|         println!("No calendar path configured, discovering calendars..."); | ||||
|  | ||||
|         // Try different common CalDAV discovery paths | ||||
|         // Note: paths should be relative to the server URL base | ||||
|         let user_calendar_path = format!("/calendars/{}/", self.config.username); | ||||
|         let user_dav_calendar_path = format!("/dav.php/calendars/{}/", self.config.username); | ||||
|          | ||||
|         let discovery_paths = vec![ | ||||
|             "/calendars/", | ||||
|             user_calendar_path.as_str(), | ||||
|             user_dav_calendar_path.as_str(), | ||||
|             "/dav.php/calendars/", | ||||
|         ]; | ||||
|  | ||||
|         let mut all_calendars = Vec::new(); | ||||
| @@ -499,6 +511,7 @@ impl CalDAVClient { | ||||
|             .map_err(CalDAVError::RequestError)?; | ||||
|  | ||||
|         if response.status().as_u16() != 207 { | ||||
|             println!("❌ Discovery PROPFIND failed for {}: HTTP {}", path, response.status().as_u16()); | ||||
|             return Err(CalDAVError::ServerError(response.status().as_u16())); | ||||
|         } | ||||
|  | ||||
| @@ -512,15 +525,33 @@ impl CalDAVClient { | ||||
|             if let Some(end_pos) = response_block.find("</d:response>") { | ||||
|                 let response_content = &response_block[..end_pos]; | ||||
|                  | ||||
|                 // Look for actual calendar collections (not just containers) | ||||
|                 if response_content.contains("<c:supported-calendar-component-set") ||  | ||||
|                    (response_content.contains("<d:collection/>") &&  | ||||
|                     response_content.contains("calendar")) { | ||||
|                     if let Some(href) = self.extract_xml_content(response_content, "href") { | ||||
|                         // Only include actual calendar paths, not container directories | ||||
|                         if href.ends_with('/') && href.contains("calendar") && !href.ends_with("/calendars/") { | ||||
|                 // Extract href first | ||||
|                 if let Some(href) = self.extract_xml_content(response_content, "href") { | ||||
|                     println!("🔍 Checking resource: {}", href); | ||||
|                      | ||||
|                     // Check if this is a calendar collection by looking for supported-calendar-component-set | ||||
|                     // This indicates it's an actual calendar that can contain events | ||||
|                     let has_supported_components = response_content.contains("supported-calendar-component-set") && | ||||
|                                                   (response_content.contains("VEVENT") || response_content.contains("VTODO")); | ||||
|                     let has_calendar_resourcetype = response_content.contains("<cal:calendar") || response_content.contains("<c:calendar"); | ||||
|                      | ||||
|                     let is_calendar = has_supported_components || has_calendar_resourcetype; | ||||
|                      | ||||
|                     // Also check resourcetype for collection | ||||
|                     let has_collection = response_content.contains("<d:collection") || response_content.contains("<collection"); | ||||
|                      | ||||
|                     if is_calendar && has_collection { | ||||
|                         // Exclude system directories like inbox, outbox, and root calendar directories | ||||
|                         if !href.contains("/inbox/") && !href.contains("/outbox/") &&  | ||||
|                            !href.ends_with("/calendars/") && href.ends_with('/') { | ||||
|                             println!("📅 Found calendar collection: {}", href); | ||||
|                             calendar_paths.push(href); | ||||
|                         } else { | ||||
|                             println!("❌ Skipping system/root directory: {}", href); | ||||
|                         } | ||||
|                     } else { | ||||
|                         println!("ℹ️  Not a calendar collection: {} (is_calendar: {}, has_collection: {})",  | ||||
|                                href, is_calendar, has_collection); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|   | ||||
| @@ -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())) | ||||
|     } | ||||
| } | ||||
| @@ -3,7 +3,6 @@ use axum::{ | ||||
|     routing::{get, post}, | ||||
|     Router, | ||||
| }; | ||||
| use sqlx::sqlite::SqlitePool; | ||||
| use tower_http::cors::{CorsLayer, Any}; | ||||
| use std::sync::Arc; | ||||
|  | ||||
| @@ -23,25 +22,12 @@ pub struct AppState { | ||||
| pub async fn run_server() -> Result<(), Box<dyn std::error::Error>> { | ||||
|     // Initialize logging | ||||
|     println!("🚀 Starting Calendar Backend Server"); | ||||
|  | ||||
|     // Set up database | ||||
|     let database_url = std::env::var("DATABASE_URL") | ||||
|         .unwrap_or_else(|_| "sqlite:calendar.db?mode=rwc".to_string()); | ||||
|      | ||||
|     let db_pool = SqlitePool::connect(&database_url).await?; | ||||
|      | ||||
|     // Run migrations - create database file if it doesn't exist | ||||
|     // The migrate!() macro looks for migrations in the current directory | ||||
|     // so we don't need to run explicit migrations here since we handle it in init_db() | ||||
|      | ||||
|     // Create auth service | ||||
|     let jwt_secret = std::env::var("JWT_SECRET") | ||||
|         .unwrap_or_else(|_| "your-super-secret-jwt-key-change-in-production".to_string()); | ||||
|      | ||||
|     let auth_service = AuthService::new(db_pool, jwt_secret); | ||||
|      | ||||
|     // Initialize database schema | ||||
|     auth_service.init_db().await?; | ||||
|     let auth_service = AuthService::new(jwt_secret); | ||||
|      | ||||
|     let app_state = AppState { auth_service }; | ||||
|  | ||||
| @@ -49,7 +35,6 @@ pub async fn run_server() -> Result<(), Box<dyn std::error::Error>> { | ||||
|     let app = Router::new() | ||||
|         .route("/", get(root)) | ||||
|         .route("/api/health", get(health_check)) | ||||
|         .route("/api/auth/register", post(handlers::register)) | ||||
|         .route("/api/auth/login", post(handlers::login)) | ||||
|         .route("/api/auth/verify", get(handlers::verify_token)) | ||||
|         .route("/api/calendar/events", get(handlers::get_calendar_events)) | ||||
|   | ||||
| @@ -3,44 +3,21 @@ use axum::{ | ||||
|     response::{IntoResponse, Response}, | ||||
|     Json, | ||||
| }; | ||||
| use chrono::{DateTime, Utc}; | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| // Database models | ||||
| #[derive(Debug, Clone, Serialize, Deserialize)] | ||||
| pub struct User { | ||||
|     pub id: String, | ||||
|     pub username: String, | ||||
|     pub email: String, | ||||
|     pub password_hash: String, | ||||
|     pub created_at: DateTime<Utc>, | ||||
| } | ||||
|  | ||||
| // API request/response types | ||||
| #[derive(Debug, Clone, Serialize, Deserialize)] | ||||
| pub struct UserInfo { | ||||
|     pub id: String, | ||||
|     pub username: String, | ||||
|     pub email: String, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Deserialize)] | ||||
| pub struct RegisterRequest { | ||||
|     pub username: String, | ||||
|     pub email: String, | ||||
|     pub password: String, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Deserialize)] | ||||
| pub struct LoginRequest { | ||||
| pub struct CalDAVLoginRequest { | ||||
|     pub username: String, | ||||
|     pub password: String, | ||||
|     pub server_url: String, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize)] | ||||
| pub struct AuthResponse { | ||||
|     pub token: String, | ||||
|     pub user: UserInfo, | ||||
|     pub username: String, | ||||
|     pub server_url: String, | ||||
| } | ||||
|  | ||||
| // Error handling | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Connor Johnstone
					Connor Johnstone