From d85898cae7902c34604641aaed21b2ee3292bff0 Mon Sep 17 00:00:00 2001 From: Connor Johnstone Date: Thu, 28 Aug 2025 18:40:22 -0400 Subject: [PATCH] Refactor authentication from database to direct CalDAV authentication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- backend/Cargo.toml | 2 - backend/src/auth.rs | 232 +++++++++--------------------- backend/src/calendar.rs | 59 ++++++-- backend/src/handlers.rs | 122 +++++++--------- backend/src/lib.rs | 17 +-- backend/src/models.rs | 31 +--- src/app.rs | 38 +++-- src/auth.rs | 33 +---- src/components/login.rs | 78 ++++++++-- src/components/mod.rs | 2 - src/components/register.rs | 235 ------------------------------- src/services/calendar_service.rs | 9 +- 12 files changed, 276 insertions(+), 582 deletions(-) delete mode 100644 src/components/register.rs diff --git a/backend/Cargo.toml b/backend/Cargo.toml index d9ffff2..40de263 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -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"] } diff --git a/backend/src/auth.rs b/backend/src/auth.rs index eb11130..43cc6e0 100644 --- a/backend/src/auth.rs +++ b/backend/src/auth.rs @@ -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 { + /// Authenticate user directly against CalDAV server + pub async fn login(&self, request: CalDAVLoginRequest) -> Result { // 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 { - // 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 { - 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 { + 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, }) } - async fn get_user_by_username(&self, username: &str) -> Result { - 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 { - 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 { - 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 { - 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 { + 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 { - 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( diff --git a/backend/src/calendar.rs b/backend/src/calendar.rs index e369f62..e59b148 100644 --- a/backend/src/calendar.rs +++ b/backend/src/calendar.rs @@ -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("") { let response_content = &response_block[..end_pos]; - // Look for actual calendar collections (not just containers) - if response_content.contains("") && - 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(">, + State(state): State>, Query(params): Query, headers: HeaderMap, ) -> Result>, 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>, + State(state): State>, Path(uid): Path, headers: HeaderMap, ) -> Result>, 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>, - Json(request): Json, -) -> Result, ApiError> { - let response = state.auth_service.register(request).await?; - Ok(Json(response)) -} - pub async fn login( State(state): State>, - Json(request): Json, + Json(request): Json, ) -> Result, 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>, headers: HeaderMap, ) -> Result, 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 { + 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 { + 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())) + } } \ No newline at end of file diff --git a/backend/src/lib.rs b/backend/src/lib.rs index 8da55cc..06e9b83 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -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> { // 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> { 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)) diff --git a/backend/src/models.rs b/backend/src/models.rs index 8e156fa..5ef83a0 100644 --- a/backend/src/models.rs +++ b/backend/src/models.rs @@ -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, -} - // 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 diff --git a/src/app.rs b/src/app.rs index 8fe6201..5e1f472 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,7 +1,7 @@ use yew::prelude::*; use yew_router::prelude::*; use gloo_storage::{LocalStorage, Storage}; -use crate::components::{Login, Register, Calendar}; +use crate::components::{Login, Calendar}; use crate::services::{CalendarService, CalendarEvent}; use std::collections::HashMap; use chrono::{Local, NaiveDate, Datelike}; @@ -12,8 +12,6 @@ enum Route { Home, #[at("/login")] Login, - #[at("/register")] - Register, #[at("/calendar")] Calendar, } @@ -56,7 +54,6 @@ pub fn App() -> Html { html! { } } @@ -83,13 +80,6 @@ pub fn App() -> Html { html! { } } } - Route::Register => { - if auth_token.is_some() { - html! { to={Route::Calendar}/> } - } else { - html! { } - } - } Route::Calendar => { if auth_token.is_some() { html! { } @@ -136,7 +126,18 @@ fn CalendarView() -> Html { wasm_bindgen_futures::spawn_local(async move { let calendar_service = CalendarService::new(); - match calendar_service.refresh_event(&token, &uid).await { + // Get password from stored credentials + let password = if let Ok(credentials_str) = LocalStorage::get::("caldav_credentials") { + if let Ok(credentials) = serde_json::from_str::(&credentials_str) { + credentials["password"].as_str().unwrap_or("").to_string() + } else { + String::new() + } + } else { + String::new() + }; + + match calendar_service.refresh_event(&token, &password, &uid).await { Ok(Some(refreshed_event)) => { // If this is a recurring event, we need to regenerate all occurrences let mut updated_events = (*events).clone(); @@ -203,7 +204,18 @@ fn CalendarView() -> Html { wasm_bindgen_futures::spawn_local(async move { let calendar_service = CalendarService::new(); - match calendar_service.fetch_events_for_month(&token, current_year, current_month).await { + // Get password from stored credentials + let password = if let Ok(credentials_str) = LocalStorage::get::("caldav_credentials") { + if let Ok(credentials) = serde_json::from_str::(&credentials_str) { + credentials["password"].as_str().unwrap_or("").to_string() + } else { + String::new() + } + } else { + String::new() + }; + + match calendar_service.fetch_events_for_month(&token, &password, current_year, current_month).await { Ok(calendar_events) => { let grouped_events = CalendarService::group_events_by_date(calendar_events); events.set(grouped_events); diff --git a/src/auth.rs b/src/auth.rs index 103debe..ba24dd8 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -4,29 +4,9 @@ use wasm_bindgen::JsCast; use wasm_bindgen_futures::JsFuture; use web_sys::{Request, RequestInit, RequestMode, Response}; -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct User { - pub id: String, - pub username: String, - pub email: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct UserInfo { - pub id: String, - pub username: String, - pub email: String, -} - #[derive(Debug, Serialize, Deserialize)] -pub struct RegisterRequest { - pub username: String, - pub email: String, - pub password: String, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct LoginRequest { +pub struct CalDAVLoginRequest { + pub server_url: String, pub username: String, pub password: String, } @@ -34,7 +14,8 @@ pub struct LoginRequest { #[derive(Debug, Serialize, Deserialize)] pub struct AuthResponse { pub token: String, - pub user: UserInfo, + pub username: String, + pub server_url: String, } #[derive(Debug, Deserialize)] @@ -57,11 +38,7 @@ impl AuthService { Self { base_url } } - pub async fn register(&self, request: RegisterRequest) -> Result { - self.post_json("/auth/register", &request).await - } - - pub async fn login(&self, request: LoginRequest) -> Result { + pub async fn login(&self, request: CalDAVLoginRequest) -> Result { self.post_json("/auth/login", &request).await } diff --git a/src/components/login.rs b/src/components/login.rs index b5dfaf3..ec2735a 100644 --- a/src/components/login.rs +++ b/src/components/login.rs @@ -9,14 +9,24 @@ pub struct LoginProps { #[function_component] pub fn Login(props: &LoginProps) -> Html { + let server_url = use_state(String::new); let username = use_state(String::new); let password = use_state(String::new); let error_message = use_state(|| Option::::None); let is_loading = use_state(|| false); + let server_url_ref = use_node_ref(); let username_ref = use_node_ref(); let password_ref = use_node_ref(); + let on_server_url_change = { + let server_url = server_url.clone(); + Callback::from(move |e: Event| { + let target = e.target_unchecked_into::(); + server_url.set(target.value()); + }) + }; + let on_username_change = { let username = username.clone(); Callback::from(move |e: Event| { @@ -34,6 +44,7 @@ pub fn Login(props: &LoginProps) -> Html { }; let on_submit = { + let server_url = server_url.clone(); let username = username.clone(); let password = password.clone(); let error_message = error_message.clone(); @@ -43,6 +54,7 @@ pub fn Login(props: &LoginProps) -> Html { Callback::from(move |e: SubmitEvent| { e.prevent_default(); + let server_url = (*server_url).clone(); let username = (*username).clone(); let password = (*password).clone(); let error_message = error_message.clone(); @@ -50,7 +62,7 @@ pub fn Login(props: &LoginProps) -> Html { let on_login = on_login.clone(); // Basic client-side validation - if username.trim().is_empty() || password.is_empty() { + if server_url.trim().is_empty() || username.trim().is_empty() || password.is_empty() { error_message.set(Some("Please fill in all fields".to_string())); return; } @@ -59,19 +71,27 @@ pub fn Login(props: &LoginProps) -> Html { error_message.set(None); wasm_bindgen_futures::spawn_local(async move { - match perform_login(username, password).await { - Ok(token) => { - // Store token in local storage + web_sys::console::log_1(&"🚀 Starting login process...".into()); + match perform_login(server_url.clone(), username.clone(), password.clone()).await { + Ok((token, credentials)) => { + web_sys::console::log_1(&"✅ Login successful!".into()); + // Store token and credentials in local storage if let Err(_) = LocalStorage::set("auth_token", &token) { error_message.set(Some("Failed to store authentication token".to_string())); is_loading.set(false); return; } + if let Err(_) = LocalStorage::set("caldav_credentials", &credentials) { + error_message.set(Some("Failed to store credentials".to_string())); + is_loading.set(false); + return; + } is_loading.set(false); on_login.emit(token); } Err(err) => { + web_sys::console::log_1(&format!("❌ Login failed: {}", err).into()); error_message.set(Some(err)); is_loading.set(false); } @@ -83,8 +103,21 @@ pub fn Login(props: &LoginProps) -> Html { html! {