use chrono::{Duration, Utc}; use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation}; use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::calendar::CalDAVClient; use crate::config::CalDAVConfig; use crate::db::{Database, PreferencesRepository, Session, SessionRepository, UserRepository}; use crate::models::{ApiError, AuthResponse, CalDAVLoginRequest, UserPreferencesResponse}; #[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, db: Database, } impl AuthService { pub fn new(jwt_secret: String, db: Database) -> Self { Self { jwt_secret, db } } /// 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::new( request.server_url.clone(), request.username.clone(), request.password.clone(), ); 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() ); // Find or create user in database let user_repo = UserRepository::new(&self.db); let user = user_repo .find_or_create(&request.username, &request.server_url) .await .map_err(|e| ApiError::Database(format!("Failed to create user: {}", e)))?; // Generate JWT token let jwt_token = self.generate_token(&request.username, &request.server_url)?; // Generate session token let session_token = format!("sess_{}", Uuid::new_v4()); // Create session in database let session = Session::new(user.id.clone(), session_token.clone(), 24); let session_repo = SessionRepository::new(&self.db); session_repo .create(&session) .await .map_err(|e| ApiError::Database(format!("Failed to create session: {}", e)))?; // Get or create user preferences let prefs_repo = PreferencesRepository::new(&self.db); let preferences = prefs_repo .get_or_create(&user.id) .await .map_err(|e| ApiError::Database(format!("Failed to get preferences: {}", e)))?; Ok(AuthResponse { token: jwt_token, session_token, username: request.username, server_url: request.server_url, preferences: UserPreferencesResponse { calendar_selected_date: preferences.calendar_selected_date, calendar_time_increment: preferences.calendar_time_increment, calendar_view_mode: preferences.calendar_view_mode, calendar_theme: preferences.calendar_theme, calendar_style: preferences.calendar_style, calendar_colors: preferences.calendar_colors, last_used_calendar: preferences.last_used_calendar, }, }) } 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) } /// Get user from token pub async fn get_user_from_token(&self, token: &str) -> Result { let claims = self.verify_token(token)?; let user_repo = UserRepository::new(&self.db); user_repo .find_or_create(&claims.username, &claims.server_url) .await .map_err(|e| ApiError::Database(format!("Failed to get user: {}", e))) } /// 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::new( claims.server_url, claims.username, password.to_string(), )) } 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) } /// Validate session token pub async fn validate_session(&self, session_token: &str) -> Result { let session_repo = SessionRepository::new(&self.db); let session = session_repo .find_by_token(session_token) .await .map_err(|e| ApiError::Database(format!("Failed to find session: {}", e)))? .ok_or_else(|| ApiError::Unauthorized("Invalid session token".to_string()))?; if session.is_expired() { return Err(ApiError::Unauthorized("Session expired".to_string())); } Ok(session.user_id) } /// Logout user by deleting session pub async fn logout(&self, session_token: &str) -> Result<(), ApiError> { let session_repo = SessionRepository::new(&self.db); session_repo .delete(session_token) .await .map_err(|e| ApiError::Database(format!("Failed to delete session: {}", e)))?; Ok(()) } }