use chrono::{Duration, Utc}; use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation}; use serde::{Deserialize, Serialize}; use crate::models::{CalDAVLoginRequest, AuthResponse, ApiError}; use crate::config::CalDAVConfig; use crate::calendar::CalDAVClient; #[derive(Debug, Serialize, Deserialize)] pub struct Claims { pub username: String, pub server_url: String, pub exp: i64, // Expiration time pub iat: i64, // Issued at } #[derive(Clone)] pub struct AuthService { jwt_secret: String, } impl AuthService { pub fn new(jwt_secret: String) -> Self { Self { jwt_secret } } /// Authenticate user directly against CalDAV server pub async fn login(&self, request: CalDAVLoginRequest) -> Result { // Validate input self.validate_login(&request)?; println!("✅ Input validation passed"); // 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"); // Test authentication against CalDAV server let caldav_client = CalDAVClient::new(caldav_config.clone()); println!("🔗 Created CalDAV client, attempting to discover calendars..."); // 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 { self.decode_token(token) } /// Create CalDAV config from token pub fn caldav_config_from_token(&self, token: &str, password: &str) -> Result { 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, }) } 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.password.trim().is_empty() { return Err(ApiError::BadRequest("Password is required".to_string())); } if request.server_url.trim().is_empty() { return Err(ApiError::BadRequest("Server URL is required".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(()) } pub fn generate_token(&self, username: &str, server_url: &str) -> Result { let now = Utc::now(); let expires_at = now + Duration::hours(24); // Token valid for 24 hours let claims = Claims { username: username.to_string(), server_url: server_url.to_string(), exp: expires_at.timestamp(), iat: now.timestamp(), }; let token = encode( &Header::default(), &claims, &EncodingKey::from_secret(self.jwt_secret.as_bytes()), ) .map_err(|e| ApiError::Internal(format!("Token generation failed: {}", e)))?; Ok(token) } fn decode_token(&self, token: &str) -> Result { let token_data = decode::( token, &DecodingKey::from_secret(self.jwt_secret.as_bytes()), &Validation::new(Algorithm::HS256), ) .map_err(|_| ApiError::Unauthorized("Invalid token".to_string()))?; Ok(token_data.claims) } }