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]
|
[dependencies]
|
||||||
# Backend authentication dependencies
|
# Backend authentication dependencies
|
||||||
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "sqlite", "chrono", "uuid", "migrate"] }
|
|
||||||
bcrypt = "0.15"
|
|
||||||
jsonwebtoken = "9.0"
|
jsonwebtoken = "9.0"
|
||||||
tokio = { version = "1.0", features = ["full"] }
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
axum = { version = "0.7", features = ["json"] }
|
axum = { version = "0.7", features = ["json"] }
|
||||||
|
|||||||
@@ -1,210 +1,118 @@
|
|||||||
use bcrypt::{hash, verify, DEFAULT_COST};
|
|
||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Utc};
|
||||||
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
|
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
|
||||||
use serde::{Deserialize, Serialize};
|
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)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct Claims {
|
pub struct Claims {
|
||||||
pub sub: String, // Subject (user ID)
|
pub username: String,
|
||||||
|
pub server_url: String,
|
||||||
pub exp: i64, // Expiration time
|
pub exp: i64, // Expiration time
|
||||||
pub iat: i64, // Issued at
|
pub iat: i64, // Issued at
|
||||||
pub username: String,
|
|
||||||
pub email: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AuthService {
|
pub struct AuthService {
|
||||||
db: SqlitePool,
|
|
||||||
jwt_secret: String,
|
jwt_secret: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AuthService {
|
impl AuthService {
|
||||||
pub fn new(db: SqlitePool, jwt_secret: String) -> Self {
|
pub fn new(jwt_secret: String) -> Self {
|
||||||
Self { db, jwt_secret }
|
Self { jwt_secret }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn init_db(&self) -> Result<(), ApiError> {
|
/// Authenticate user directly against CalDAV server
|
||||||
sqlx::query(
|
pub async fn login(&self, request: CalDAVLoginRequest) -> Result<AuthResponse, ApiError> {
|
||||||
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> {
|
|
||||||
// Validate input
|
// Validate input
|
||||||
self.validate_registration(&request)?;
|
self.validate_login(&request)?;
|
||||||
|
println!("✅ Input validation passed");
|
||||||
|
|
||||||
// Check if user already exists
|
// Create CalDAV config with provided credentials
|
||||||
if self.user_exists(&request.username, &request.email).await? {
|
let caldav_config = CalDAVConfig {
|
||||||
return Err(ApiError::Conflict("Username or email already exists".to_string()));
|
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
|
// Test authentication against CalDAV server
|
||||||
let password_hash = hash(&request.password, DEFAULT_COST)
|
let caldav_client = CalDAVClient::new(caldav_config.clone());
|
||||||
.map_err(|e| ApiError::Internal(format!("Password hashing failed: {}", e)))?;
|
println!("🔗 Created CalDAV client, attempting to discover calendars...");
|
||||||
|
|
||||||
// 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?;
|
|
||||||
|
|
||||||
Ok(UserInfo {
|
// Try to discover calendars as an authentication test
|
||||||
id: user.id,
|
match caldav_client.discover_calendars().await {
|
||||||
username: user.username,
|
Ok(calendars) => {
|
||||||
email: user.email,
|
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> {
|
fn validate_login(&self, request: &CalDAVLoginRequest) -> Result<(), 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> {
|
|
||||||
if request.username.trim().is_empty() {
|
if request.username.trim().is_empty() {
|
||||||
return Err(ApiError::BadRequest("Username is required".to_string()));
|
return Err(ApiError::BadRequest("Username is required".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if request.username.len() < 3 {
|
if request.password.trim().is_empty() {
|
||||||
return Err(ApiError::BadRequest("Username must be at least 3 characters".to_string()));
|
return Err(ApiError::BadRequest("Password is required".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if request.email.trim().is_empty() || !request.email.contains('@') {
|
if request.server_url.trim().is_empty() {
|
||||||
return Err(ApiError::BadRequest("Valid email is required".to_string()));
|
return Err(ApiError::BadRequest("Server URL is required".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if request.password.len() < 6 {
|
// Basic URL validation
|
||||||
return Err(ApiError::BadRequest("Password must be at least 6 characters".to_string()));
|
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(())
|
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 now = Utc::now();
|
||||||
let expires_at = now + Duration::hours(24); // Token valid for 24 hours
|
let expires_at = now + Duration::hours(24); // Token valid for 24 hours
|
||||||
|
|
||||||
let claims = Claims {
|
let claims = Claims {
|
||||||
sub: user.id.clone(),
|
username: username.to_string(),
|
||||||
|
server_url: server_url.to_string(),
|
||||||
exp: expires_at.timestamp(),
|
exp: expires_at.timestamp(),
|
||||||
iat: now.timestamp(),
|
iat: now.timestamp(),
|
||||||
username: user.username.clone(),
|
|
||||||
email: user.email.clone(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let token = encode(
|
let token = encode(
|
||||||
|
|||||||
@@ -149,12 +149,26 @@ impl CalDAVClient {
|
|||||||
let url = if calendar_path.starts_with("http") {
|
let url = if calendar_path.starts_with("http") {
|
||||||
calendar_path.to_string()
|
calendar_path.to_string()
|
||||||
} else {
|
} 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
|
let response = self.http_client
|
||||||
.request(reqwest::Method::from_bytes(b"REPORT").unwrap(), &url)
|
.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("Content-Type", "application/xml")
|
||||||
.header("Depth", "1")
|
.header("Depth", "1")
|
||||||
.header("User-Agent", "calendar-app/0.1.0")
|
.header("User-Agent", "calendar-app/0.1.0")
|
||||||
@@ -296,7 +310,7 @@ impl CalDAVClient {
|
|||||||
// Parse end time (optional - use start time if not present)
|
// Parse end time (optional - use start time if not present)
|
||||||
let end = if let Some(dtend) = properties.get("DTEND") {
|
let end = if let Some(dtend) = properties.get("DTEND") {
|
||||||
Some(self.parse_datetime(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
|
// TODO: Parse duration and add to start time
|
||||||
Some(start)
|
Some(start)
|
||||||
} else {
|
} else {
|
||||||
@@ -443,18 +457,16 @@ impl CalDAVClient {
|
|||||||
println!("Using configured calendar path: {}", calendar_path);
|
println!("Using configured calendar path: {}", calendar_path);
|
||||||
return Ok(vec![calendar_path.clone()]);
|
return Ok(vec![calendar_path.clone()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("No calendar path configured, discovering calendars...");
|
println!("No calendar path configured, discovering calendars...");
|
||||||
|
|
||||||
// Try different common CalDAV discovery paths
|
// 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_calendar_path = format!("/calendars/{}/", self.config.username);
|
||||||
let user_dav_calendar_path = format!("/dav.php/calendars/{}/", self.config.username);
|
|
||||||
|
|
||||||
let discovery_paths = vec![
|
let discovery_paths = vec![
|
||||||
"/calendars/",
|
"/calendars/",
|
||||||
user_calendar_path.as_str(),
|
user_calendar_path.as_str(),
|
||||||
user_dav_calendar_path.as_str(),
|
|
||||||
"/dav.php/calendars/",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
let mut all_calendars = Vec::new();
|
let mut all_calendars = Vec::new();
|
||||||
@@ -499,6 +511,7 @@ impl CalDAVClient {
|
|||||||
.map_err(CalDAVError::RequestError)?;
|
.map_err(CalDAVError::RequestError)?;
|
||||||
|
|
||||||
if response.status().as_u16() != 207 {
|
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()));
|
return Err(CalDAVError::ServerError(response.status().as_u16()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -512,15 +525,33 @@ impl CalDAVClient {
|
|||||||
if let Some(end_pos) = response_block.find("</d:response>") {
|
if let Some(end_pos) = response_block.find("</d:response>") {
|
||||||
let response_content = &response_block[..end_pos];
|
let response_content = &response_block[..end_pos];
|
||||||
|
|
||||||
// Look for actual calendar collections (not just containers)
|
// Extract href first
|
||||||
if response_content.contains("<c:supported-calendar-component-set") ||
|
if let Some(href) = self.extract_xml_content(response_content, "href") {
|
||||||
(response_content.contains("<d:collection/>") &&
|
println!("🔍 Checking resource: {}", href);
|
||||||
response_content.contains("calendar")) {
|
|
||||||
if let Some(href) = self.extract_xml_content(response_content, "href") {
|
// Check if this is a calendar collection by looking for supported-calendar-component-set
|
||||||
// Only include actual calendar paths, not container directories
|
// This indicates it's an actual calendar that can contain events
|
||||||
if href.ends_with('/') && href.contains("calendar") && !href.ends_with("/calendars/") {
|
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);
|
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 std::sync::Arc;
|
||||||
use chrono::Datelike;
|
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::calendar::{CalDAVClient, CalendarEvent};
|
||||||
use crate::config::CalDAVConfig;
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct CalendarQuery {
|
pub struct CalendarQuery {
|
||||||
@@ -18,31 +17,17 @@ pub struct CalendarQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_calendar_events(
|
pub async fn get_calendar_events(
|
||||||
State(_state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Query(params): Query<CalendarQuery>,
|
Query(params): Query<CalendarQuery>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
) -> Result<Json<Vec<CalendarEvent>>, ApiError> {
|
) -> Result<Json<Vec<CalendarEvent>>, ApiError> {
|
||||||
// Verify authentication (extract token from Authorization header)
|
// Extract and verify token
|
||||||
let _token = if let Some(auth_header) = headers.get("authorization") {
|
let token = extract_bearer_token(&headers)?;
|
||||||
let auth_str = auth_header
|
let password = extract_password_header(&headers)?;
|
||||||
.to_str()
|
println!("🔑 API call with password length: {}", password.len());
|
||||||
.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)))?;
|
|
||||||
|
|
||||||
|
// Create CalDAV config from token and password
|
||||||
|
let config = state.auth_service.caldav_config_from_token(&token, &password)?;
|
||||||
let client = CalDAVClient::new(config);
|
let client = CalDAVClient::new(config);
|
||||||
|
|
||||||
// Discover calendars if needed
|
// Discover calendars if needed
|
||||||
@@ -74,31 +59,16 @@ pub async fn get_calendar_events(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn refresh_event(
|
pub async fn refresh_event(
|
||||||
State(_state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(uid): Path<String>,
|
Path(uid): Path<String>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
) -> Result<Json<Option<CalendarEvent>>, ApiError> {
|
) -> Result<Json<Option<CalendarEvent>>, ApiError> {
|
||||||
// Verify authentication (extract token from Authorization header)
|
// Extract and verify token
|
||||||
let _token = if let Some(auth_header) = headers.get("authorization") {
|
let token = extract_bearer_token(&headers)?;
|
||||||
let auth_str = auth_header
|
let password = extract_password_header(&headers)?;
|
||||||
.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)))?;
|
|
||||||
|
|
||||||
|
// Create CalDAV config from token and password
|
||||||
|
let config = state.auth_service.caldav_config_from_token(&token, &password)?;
|
||||||
let client = CalDAVClient::new(config);
|
let client = CalDAVClient::new(config);
|
||||||
|
|
||||||
// Discover calendars if needed
|
// Discover calendars if needed
|
||||||
@@ -119,18 +89,15 @@ pub async fn refresh_event(
|
|||||||
Ok(Json(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(
|
pub async fn login(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Json(request): Json<LoginRequest>,
|
Json(request): Json<CalDAVLoginRequest>,
|
||||||
) -> Result<Json<AuthResponse>, ApiError> {
|
) -> 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?;
|
let response = state.auth_service.login(request).await?;
|
||||||
Ok(Json(response))
|
Ok(Json(response))
|
||||||
}
|
}
|
||||||
@@ -139,25 +106,40 @@ pub async fn verify_token(
|
|||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||||
// Try to get token from Authorization header
|
let token = extract_bearer_token(&headers)?;
|
||||||
let token = if let Some(auth_header) = headers.get("authorization") {
|
let claims = state.auth_service.verify_token(&token)?;
|
||||||
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?;
|
|
||||||
|
|
||||||
Ok(Json(serde_json::json!({
|
Ok(Json(serde_json::json!({
|
||||||
"valid": true,
|
"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},
|
routing::{get, post},
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
use sqlx::sqlite::SqlitePool;
|
|
||||||
use tower_http::cors::{CorsLayer, Any};
|
use tower_http::cors::{CorsLayer, Any};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@@ -23,25 +22,12 @@ pub struct AppState {
|
|||||||
pub async fn run_server() -> Result<(), Box<dyn std::error::Error>> {
|
pub async fn run_server() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
// Initialize logging
|
// Initialize logging
|
||||||
println!("🚀 Starting Calendar Backend Server");
|
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
|
// Create auth service
|
||||||
let jwt_secret = std::env::var("JWT_SECRET")
|
let jwt_secret = std::env::var("JWT_SECRET")
|
||||||
.unwrap_or_else(|_| "your-super-secret-jwt-key-change-in-production".to_string());
|
.unwrap_or_else(|_| "your-super-secret-jwt-key-change-in-production".to_string());
|
||||||
|
|
||||||
let auth_service = AuthService::new(db_pool, jwt_secret);
|
let auth_service = AuthService::new(jwt_secret);
|
||||||
|
|
||||||
// Initialize database schema
|
|
||||||
auth_service.init_db().await?;
|
|
||||||
|
|
||||||
let app_state = AppState { auth_service };
|
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()
|
let app = Router::new()
|
||||||
.route("/", get(root))
|
.route("/", get(root))
|
||||||
.route("/api/health", get(health_check))
|
.route("/api/health", get(health_check))
|
||||||
.route("/api/auth/register", post(handlers::register))
|
|
||||||
.route("/api/auth/login", post(handlers::login))
|
.route("/api/auth/login", post(handlers::login))
|
||||||
.route("/api/auth/verify", get(handlers::verify_token))
|
.route("/api/auth/verify", get(handlers::verify_token))
|
||||||
.route("/api/calendar/events", get(handlers::get_calendar_events))
|
.route("/api/calendar/events", get(handlers::get_calendar_events))
|
||||||
|
|||||||
@@ -3,44 +3,21 @@ use axum::{
|
|||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
Json,
|
Json,
|
||||||
};
|
};
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use serde::{Deserialize, Serialize};
|
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
|
// API request/response types
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct UserInfo {
|
|
||||||
pub id: String,
|
|
||||||
pub username: String,
|
|
||||||
pub email: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct RegisterRequest {
|
pub struct CalDAVLoginRequest {
|
||||||
pub username: String,
|
|
||||||
pub email: String,
|
|
||||||
pub password: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct LoginRequest {
|
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
|
pub server_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct AuthResponse {
|
pub struct AuthResponse {
|
||||||
pub token: String,
|
pub token: String,
|
||||||
pub user: UserInfo,
|
pub username: String,
|
||||||
|
pub server_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error handling
|
// Error handling
|
||||||
|
|||||||
38
src/app.rs
38
src/app.rs
@@ -1,7 +1,7 @@
|
|||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
use yew_router::prelude::*;
|
use yew_router::prelude::*;
|
||||||
use gloo_storage::{LocalStorage, Storage};
|
use gloo_storage::{LocalStorage, Storage};
|
||||||
use crate::components::{Login, Register, Calendar};
|
use crate::components::{Login, Calendar};
|
||||||
use crate::services::{CalendarService, CalendarEvent};
|
use crate::services::{CalendarService, CalendarEvent};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use chrono::{Local, NaiveDate, Datelike};
|
use chrono::{Local, NaiveDate, Datelike};
|
||||||
@@ -12,8 +12,6 @@ enum Route {
|
|||||||
Home,
|
Home,
|
||||||
#[at("/login")]
|
#[at("/login")]
|
||||||
Login,
|
Login,
|
||||||
#[at("/register")]
|
|
||||||
Register,
|
|
||||||
#[at("/calendar")]
|
#[at("/calendar")]
|
||||||
Calendar,
|
Calendar,
|
||||||
}
|
}
|
||||||
@@ -56,7 +54,6 @@ pub fn App() -> Html {
|
|||||||
html! {
|
html! {
|
||||||
<nav>
|
<nav>
|
||||||
<Link<Route> to={Route::Login}>{"Login"}</Link<Route>>
|
<Link<Route> to={Route::Login}>{"Login"}</Link<Route>>
|
||||||
<Link<Route> to={Route::Register}>{"Register"}</Link<Route>>
|
|
||||||
</nav>
|
</nav>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,13 +80,6 @@ pub fn App() -> Html {
|
|||||||
html! { <Login {on_login} /> }
|
html! { <Login {on_login} /> }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Route::Register => {
|
|
||||||
if auth_token.is_some() {
|
|
||||||
html! { <Redirect<Route> to={Route::Calendar}/> }
|
|
||||||
} else {
|
|
||||||
html! { <Register on_register={on_login.clone()} /> }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Route::Calendar => {
|
Route::Calendar => {
|
||||||
if auth_token.is_some() {
|
if auth_token.is_some() {
|
||||||
html! { <CalendarView /> }
|
html! { <CalendarView /> }
|
||||||
@@ -136,7 +126,18 @@ fn CalendarView() -> Html {
|
|||||||
wasm_bindgen_futures::spawn_local(async move {
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
let calendar_service = CalendarService::new();
|
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::<String>("caldav_credentials") {
|
||||||
|
if let Ok(credentials) = serde_json::from_str::<serde_json::Value>(&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)) => {
|
Ok(Some(refreshed_event)) => {
|
||||||
// If this is a recurring event, we need to regenerate all occurrences
|
// If this is a recurring event, we need to regenerate all occurrences
|
||||||
let mut updated_events = (*events).clone();
|
let mut updated_events = (*events).clone();
|
||||||
@@ -203,7 +204,18 @@ fn CalendarView() -> Html {
|
|||||||
wasm_bindgen_futures::spawn_local(async move {
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
let calendar_service = CalendarService::new();
|
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::<String>("caldav_credentials") {
|
||||||
|
if let Ok(credentials) = serde_json::from_str::<serde_json::Value>(&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) => {
|
Ok(calendar_events) => {
|
||||||
let grouped_events = CalendarService::group_events_by_date(calendar_events);
|
let grouped_events = CalendarService::group_events_by_date(calendar_events);
|
||||||
events.set(grouped_events);
|
events.set(grouped_events);
|
||||||
|
|||||||
33
src/auth.rs
33
src/auth.rs
@@ -4,29 +4,9 @@ use wasm_bindgen::JsCast;
|
|||||||
use wasm_bindgen_futures::JsFuture;
|
use wasm_bindgen_futures::JsFuture;
|
||||||
use web_sys::{Request, RequestInit, RequestMode, Response};
|
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)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct RegisterRequest {
|
pub struct CalDAVLoginRequest {
|
||||||
pub username: String,
|
pub server_url: String,
|
||||||
pub email: String,
|
|
||||||
pub password: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct LoginRequest {
|
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
}
|
}
|
||||||
@@ -34,7 +14,8 @@ pub struct LoginRequest {
|
|||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct AuthResponse {
|
pub struct AuthResponse {
|
||||||
pub token: String,
|
pub token: String,
|
||||||
pub user: UserInfo,
|
pub username: String,
|
||||||
|
pub server_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@@ -57,11 +38,7 @@ impl AuthService {
|
|||||||
Self { base_url }
|
Self { base_url }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn register(&self, request: RegisterRequest) -> Result<AuthResponse, String> {
|
pub async fn login(&self, request: CalDAVLoginRequest) -> Result<AuthResponse, String> {
|
||||||
self.post_json("/auth/register", &request).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn login(&self, request: LoginRequest) -> Result<AuthResponse, String> {
|
|
||||||
self.post_json("/auth/login", &request).await
|
self.post_json("/auth/login", &request).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,14 +9,24 @@ pub struct LoginProps {
|
|||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
pub fn Login(props: &LoginProps) -> Html {
|
pub fn Login(props: &LoginProps) -> Html {
|
||||||
|
let server_url = use_state(String::new);
|
||||||
let username = use_state(String::new);
|
let username = use_state(String::new);
|
||||||
let password = use_state(String::new);
|
let password = use_state(String::new);
|
||||||
let error_message = use_state(|| Option::<String>::None);
|
let error_message = use_state(|| Option::<String>::None);
|
||||||
let is_loading = use_state(|| false);
|
let is_loading = use_state(|| false);
|
||||||
|
|
||||||
|
let server_url_ref = use_node_ref();
|
||||||
let username_ref = use_node_ref();
|
let username_ref = use_node_ref();
|
||||||
let password_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::<HtmlInputElement>();
|
||||||
|
server_url.set(target.value());
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
let on_username_change = {
|
let on_username_change = {
|
||||||
let username = username.clone();
|
let username = username.clone();
|
||||||
Callback::from(move |e: Event| {
|
Callback::from(move |e: Event| {
|
||||||
@@ -34,6 +44,7 @@ pub fn Login(props: &LoginProps) -> Html {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let on_submit = {
|
let on_submit = {
|
||||||
|
let server_url = server_url.clone();
|
||||||
let username = username.clone();
|
let username = username.clone();
|
||||||
let password = password.clone();
|
let password = password.clone();
|
||||||
let error_message = error_message.clone();
|
let error_message = error_message.clone();
|
||||||
@@ -43,6 +54,7 @@ pub fn Login(props: &LoginProps) -> Html {
|
|||||||
Callback::from(move |e: SubmitEvent| {
|
Callback::from(move |e: SubmitEvent| {
|
||||||
e.prevent_default();
|
e.prevent_default();
|
||||||
|
|
||||||
|
let server_url = (*server_url).clone();
|
||||||
let username = (*username).clone();
|
let username = (*username).clone();
|
||||||
let password = (*password).clone();
|
let password = (*password).clone();
|
||||||
let error_message = error_message.clone();
|
let error_message = error_message.clone();
|
||||||
@@ -50,7 +62,7 @@ pub fn Login(props: &LoginProps) -> Html {
|
|||||||
let on_login = on_login.clone();
|
let on_login = on_login.clone();
|
||||||
|
|
||||||
// Basic client-side validation
|
// 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()));
|
error_message.set(Some("Please fill in all fields".to_string()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -59,19 +71,27 @@ pub fn Login(props: &LoginProps) -> Html {
|
|||||||
error_message.set(None);
|
error_message.set(None);
|
||||||
|
|
||||||
wasm_bindgen_futures::spawn_local(async move {
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
match perform_login(username, password).await {
|
web_sys::console::log_1(&"🚀 Starting login process...".into());
|
||||||
Ok(token) => {
|
match perform_login(server_url.clone(), username.clone(), password.clone()).await {
|
||||||
// Store token in local storage
|
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) {
|
if let Err(_) = LocalStorage::set("auth_token", &token) {
|
||||||
error_message.set(Some("Failed to store authentication token".to_string()));
|
error_message.set(Some("Failed to store authentication token".to_string()));
|
||||||
is_loading.set(false);
|
is_loading.set(false);
|
||||||
return;
|
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);
|
is_loading.set(false);
|
||||||
on_login.emit(token);
|
on_login.emit(token);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
web_sys::console::log_1(&format!("❌ Login failed: {}", err).into());
|
||||||
error_message.set(Some(err));
|
error_message.set(Some(err));
|
||||||
is_loading.set(false);
|
is_loading.set(false);
|
||||||
}
|
}
|
||||||
@@ -83,8 +103,21 @@ pub fn Login(props: &LoginProps) -> Html {
|
|||||||
html! {
|
html! {
|
||||||
<div class="login-container">
|
<div class="login-container">
|
||||||
<div class="login-form">
|
<div class="login-form">
|
||||||
<h2>{"Sign In"}</h2>
|
<h2>{"Sign In to CalDAV"}</h2>
|
||||||
<form onsubmit={on_submit}>
|
<form onsubmit={on_submit}>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="server_url">{"CalDAV Server URL"}</label>
|
||||||
|
<input
|
||||||
|
ref={server_url_ref}
|
||||||
|
type="text"
|
||||||
|
id="server_url"
|
||||||
|
placeholder="https://your-caldav-server.com/dav/"
|
||||||
|
value={(*server_url).clone()}
|
||||||
|
onchange={on_server_url_change}
|
||||||
|
disabled={*is_loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="username">{"Username"}</label>
|
<label for="username">{"Username"}</label>
|
||||||
<input
|
<input
|
||||||
@@ -131,22 +164,43 @@ pub fn Login(props: &LoginProps) -> Html {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="auth-links">
|
<div class="auth-links">
|
||||||
<p>{"Don't have an account? "}<a href="/register">{"Sign up here"}</a></p>
|
<p>{"Enter your CalDAV server credentials to connect to your calendar"}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Perform login using the auth service
|
/// Perform login using the CalDAV auth service
|
||||||
async fn perform_login(username: String, password: String) -> Result<String, String> {
|
async fn perform_login(server_url: String, username: String, password: String) -> Result<(String, String), String> {
|
||||||
use crate::auth::{AuthService, LoginRequest};
|
use crate::auth::{AuthService, CalDAVLoginRequest};
|
||||||
|
use serde_json;
|
||||||
|
|
||||||
|
web_sys::console::log_1(&format!("📡 Creating auth service and request...").into());
|
||||||
|
|
||||||
let auth_service = AuthService::new();
|
let auth_service = AuthService::new();
|
||||||
let request = LoginRequest { username, password };
|
let request = CalDAVLoginRequest {
|
||||||
|
server_url: server_url.clone(),
|
||||||
|
username: username.clone(),
|
||||||
|
password: password.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
web_sys::console::log_1(&format!("🚀 Sending login request to backend...").into());
|
||||||
|
|
||||||
match auth_service.login(request).await {
|
match auth_service.login(request).await {
|
||||||
Ok(response) => Ok(response.token),
|
Ok(response) => {
|
||||||
Err(err) => Err(err),
|
web_sys::console::log_1(&format!("✅ Backend responded successfully").into());
|
||||||
|
// Create credentials object to store
|
||||||
|
let credentials = serde_json::json!({
|
||||||
|
"server_url": server_url,
|
||||||
|
"username": username,
|
||||||
|
"password": password
|
||||||
|
});
|
||||||
|
Ok((response.token, credentials.to_string()))
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
web_sys::console::log_1(&format!("❌ Backend error: {}", err).into());
|
||||||
|
Err(err)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
pub mod login;
|
pub mod login;
|
||||||
pub mod register;
|
|
||||||
pub mod calendar;
|
pub mod calendar;
|
||||||
pub mod event_modal;
|
pub mod event_modal;
|
||||||
|
|
||||||
pub use login::Login;
|
pub use login::Login;
|
||||||
pub use register::Register;
|
|
||||||
pub use calendar::Calendar;
|
pub use calendar::Calendar;
|
||||||
pub use event_modal::EventModal;
|
pub use event_modal::EventModal;
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
use yew::prelude::*;
|
|
||||||
use web_sys::HtmlInputElement;
|
|
||||||
use gloo_storage::{LocalStorage, Storage};
|
|
||||||
|
|
||||||
#[derive(Properties, PartialEq)]
|
|
||||||
pub struct RegisterProps {
|
|
||||||
pub on_register: Callback<String>, // Callback with JWT token
|
|
||||||
}
|
|
||||||
|
|
||||||
#[function_component]
|
|
||||||
pub fn Register(props: &RegisterProps) -> Html {
|
|
||||||
let username = use_state(String::new);
|
|
||||||
let email = use_state(String::new);
|
|
||||||
let password = use_state(String::new);
|
|
||||||
let confirm_password = use_state(String::new);
|
|
||||||
let error_message = use_state(|| Option::<String>::None);
|
|
||||||
let is_loading = use_state(|| false);
|
|
||||||
|
|
||||||
let username_ref = use_node_ref();
|
|
||||||
let email_ref = use_node_ref();
|
|
||||||
let password_ref = use_node_ref();
|
|
||||||
let confirm_password_ref = use_node_ref();
|
|
||||||
|
|
||||||
let on_username_change = {
|
|
||||||
let username = username.clone();
|
|
||||||
Callback::from(move |e: Event| {
|
|
||||||
let target = e.target_unchecked_into::<HtmlInputElement>();
|
|
||||||
username.set(target.value());
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let on_email_change = {
|
|
||||||
let email = email.clone();
|
|
||||||
Callback::from(move |e: Event| {
|
|
||||||
let target = e.target_unchecked_into::<HtmlInputElement>();
|
|
||||||
email.set(target.value());
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let on_password_change = {
|
|
||||||
let password = password.clone();
|
|
||||||
Callback::from(move |e: Event| {
|
|
||||||
let target = e.target_unchecked_into::<HtmlInputElement>();
|
|
||||||
password.set(target.value());
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let on_confirm_password_change = {
|
|
||||||
let confirm_password = confirm_password.clone();
|
|
||||||
Callback::from(move |e: Event| {
|
|
||||||
let target = e.target_unchecked_into::<HtmlInputElement>();
|
|
||||||
confirm_password.set(target.value());
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let on_submit = {
|
|
||||||
let username = username.clone();
|
|
||||||
let email = email.clone();
|
|
||||||
let password = password.clone();
|
|
||||||
let confirm_password = confirm_password.clone();
|
|
||||||
let error_message = error_message.clone();
|
|
||||||
let is_loading = is_loading.clone();
|
|
||||||
let on_register = props.on_register.clone();
|
|
||||||
|
|
||||||
Callback::from(move |e: SubmitEvent| {
|
|
||||||
e.prevent_default();
|
|
||||||
|
|
||||||
let username = (*username).clone();
|
|
||||||
let email = (*email).clone();
|
|
||||||
let password = (*password).clone();
|
|
||||||
let confirm_password = (*confirm_password).clone();
|
|
||||||
let error_message = error_message.clone();
|
|
||||||
let is_loading = is_loading.clone();
|
|
||||||
let on_register = on_register.clone();
|
|
||||||
|
|
||||||
// Client-side validation
|
|
||||||
if let Err(validation_error) = validate_registration(&username, &email, &password, &confirm_password) {
|
|
||||||
error_message.set(Some(validation_error));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
is_loading.set(true);
|
|
||||||
error_message.set(None);
|
|
||||||
|
|
||||||
wasm_bindgen_futures::spawn_local(async move {
|
|
||||||
match perform_registration(username, email, password).await {
|
|
||||||
Ok(token) => {
|
|
||||||
// Store token 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
is_loading.set(false);
|
|
||||||
on_register.emit(token);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
error_message.set(Some(err));
|
|
||||||
is_loading.set(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
html! {
|
|
||||||
<div class="register-container">
|
|
||||||
<div class="register-form">
|
|
||||||
<h2>{"Create Account"}</h2>
|
|
||||||
<form onsubmit={on_submit}>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="username">{"Username"}</label>
|
|
||||||
<input
|
|
||||||
ref={username_ref}
|
|
||||||
type="text"
|
|
||||||
id="username"
|
|
||||||
placeholder="Choose a username"
|
|
||||||
value={(*username).clone()}
|
|
||||||
onchange={on_username_change}
|
|
||||||
disabled={*is_loading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="email">{"Email"}</label>
|
|
||||||
<input
|
|
||||||
ref={email_ref}
|
|
||||||
type="email"
|
|
||||||
id="email"
|
|
||||||
placeholder="Enter your email"
|
|
||||||
value={(*email).clone()}
|
|
||||||
onchange={on_email_change}
|
|
||||||
disabled={*is_loading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="password">{"Password"}</label>
|
|
||||||
<input
|
|
||||||
ref={password_ref}
|
|
||||||
type="password"
|
|
||||||
id="password"
|
|
||||||
placeholder="Choose a password"
|
|
||||||
value={(*password).clone()}
|
|
||||||
onchange={on_password_change}
|
|
||||||
disabled={*is_loading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="confirm-password">{"Confirm Password"}</label>
|
|
||||||
<input
|
|
||||||
ref={confirm_password_ref}
|
|
||||||
type="password"
|
|
||||||
id="confirm-password"
|
|
||||||
placeholder="Confirm your password"
|
|
||||||
value={(*confirm_password).clone()}
|
|
||||||
onchange={on_confirm_password_change}
|
|
||||||
disabled={*is_loading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
if let Some(error) = (*error_message).clone() {
|
|
||||||
html! { <div class="error-message">{error}</div> }
|
|
||||||
} else {
|
|
||||||
html! {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
<button type="submit" disabled={*is_loading} class="register-button">
|
|
||||||
{
|
|
||||||
if *is_loading {
|
|
||||||
"Creating Account..."
|
|
||||||
} else {
|
|
||||||
"Create Account"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="auth-links">
|
|
||||||
<p>{"Already have an account? "}<a href="/login">{"Sign in here"}</a></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validate registration form data
|
|
||||||
fn validate_registration(username: &str, email: &str, password: &str, confirm_password: &str) -> Result<(), String> {
|
|
||||||
if username.trim().is_empty() {
|
|
||||||
return Err("Username is required".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
if username.len() < 3 {
|
|
||||||
return Err("Username must be at least 3 characters long".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
if email.trim().is_empty() {
|
|
||||||
return Err("Email is required".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
if !email.contains('@') {
|
|
||||||
return Err("Please enter a valid email address".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
if password.is_empty() {
|
|
||||||
return Err("Password is required".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
if password.len() < 6 {
|
|
||||||
return Err("Password must be at least 6 characters long".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
if password != confirm_password {
|
|
||||||
return Err("Passwords do not match".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Perform registration using the auth service
|
|
||||||
async fn perform_registration(username: String, email: String, password: String) -> Result<String, String> {
|
|
||||||
use crate::auth::{AuthService, RegisterRequest};
|
|
||||||
|
|
||||||
let auth_service = AuthService::new();
|
|
||||||
let request = RegisterRequest { username, email, password };
|
|
||||||
|
|
||||||
match auth_service.register(request).await {
|
|
||||||
Ok(response) => Ok(response.token),
|
|
||||||
Err(err) => Err(err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -139,6 +139,7 @@ impl CalendarService {
|
|||||||
pub async fn fetch_events_for_month(
|
pub async fn fetch_events_for_month(
|
||||||
&self,
|
&self,
|
||||||
token: &str,
|
token: &str,
|
||||||
|
password: &str,
|
||||||
year: i32,
|
year: i32,
|
||||||
month: u32
|
month: u32
|
||||||
) -> Result<Vec<CalendarEvent>, String> {
|
) -> Result<Vec<CalendarEvent>, String> {
|
||||||
@@ -154,6 +155,9 @@ impl CalendarService {
|
|||||||
|
|
||||||
request.headers().set("Authorization", &format!("Bearer {}", token))
|
request.headers().set("Authorization", &format!("Bearer {}", token))
|
||||||
.map_err(|e| format!("Authorization header setting failed: {:?}", e))?;
|
.map_err(|e| format!("Authorization header setting failed: {:?}", e))?;
|
||||||
|
|
||||||
|
request.headers().set("X-CalDAV-Password", password)
|
||||||
|
.map_err(|e| format!("Password header setting failed: {:?}", e))?;
|
||||||
|
|
||||||
let resp_value = JsFuture::from(window.fetch_with_request(&request))
|
let resp_value = JsFuture::from(window.fetch_with_request(&request))
|
||||||
.await
|
.await
|
||||||
@@ -407,7 +411,7 @@ impl CalendarService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Refresh a single event by UID from the CalDAV server
|
/// Refresh a single event by UID from the CalDAV server
|
||||||
pub async fn refresh_event(&self, token: &str, uid: &str) -> Result<Option<CalendarEvent>, String> {
|
pub async fn refresh_event(&self, token: &str, password: &str, uid: &str) -> Result<Option<CalendarEvent>, String> {
|
||||||
let window = web_sys::window().ok_or("No global window exists")?;
|
let window = web_sys::window().ok_or("No global window exists")?;
|
||||||
|
|
||||||
let opts = RequestInit::new();
|
let opts = RequestInit::new();
|
||||||
@@ -420,6 +424,9 @@ impl CalendarService {
|
|||||||
|
|
||||||
request.headers().set("Authorization", &format!("Bearer {}", token))
|
request.headers().set("Authorization", &format!("Bearer {}", token))
|
||||||
.map_err(|e| format!("Authorization header setting failed: {:?}", e))?;
|
.map_err(|e| format!("Authorization header setting failed: {:?}", e))?;
|
||||||
|
|
||||||
|
request.headers().set("X-CalDAV-Password", password)
|
||||||
|
.map_err(|e| format!("Password header setting failed: {:?}", e))?;
|
||||||
|
|
||||||
let resp_value = JsFuture::from(window.fetch_with_request(&request))
|
let resp_value = JsFuture::from(window.fetch_with_request(&request))
|
||||||
.await
|
.await
|
||||||
|
|||||||
Reference in New Issue
Block a user