- Split 1921-line handlers.rs into focused modules: - handlers/auth.rs: Authentication handlers (login, verify_token, get_user_info) - handlers/calendar.rs: Calendar management (create_calendar, delete_calendar) - handlers/events.rs: Event operations (CRUD operations, fetch events) - handlers/series.rs: Event series operations (recurring events management) - Main handlers.rs now serves as clean re-export module - All tests passing (14 integration + 7 unit + 3 doc tests) - Maintains backward compatibility with existing API routes - Improves code organization and separation of concerns 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
138 lines
4.7 KiB
Rust
138 lines
4.7 KiB
Rust
use chrono::{Duration, Utc};
|
|
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::models::{CalDAVLoginRequest, AuthResponse, ApiError};
|
|
use crate::config::CalDAVConfig;
|
|
use crate::calendar::CalDAVClient;
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct Claims {
|
|
pub username: String,
|
|
pub server_url: String,
|
|
pub exp: i64, // Expiration time
|
|
pub iat: i64, // Issued at
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct AuthService {
|
|
jwt_secret: String,
|
|
}
|
|
|
|
impl AuthService {
|
|
pub fn new(jwt_secret: String) -> Self {
|
|
Self { jwt_secret }
|
|
}
|
|
|
|
/// Authenticate user directly against CalDAV server
|
|
pub async fn login(&self, request: CalDAVLoginRequest) -> Result<AuthResponse, ApiError> {
|
|
// Validate input
|
|
self.validate_login(&request)?;
|
|
println!("✅ Input validation passed");
|
|
|
|
// Create CalDAV config with provided credentials
|
|
let caldav_config = CalDAVConfig {
|
|
server_url: request.server_url.clone(),
|
|
username: request.username.clone(),
|
|
password: request.password.clone(),
|
|
calendar_path: None,
|
|
tasks_path: None,
|
|
};
|
|
println!("📝 Created CalDAV config");
|
|
|
|
// Test authentication against CalDAV server
|
|
let caldav_client = CalDAVClient::new(caldav_config.clone());
|
|
println!("🔗 Created CalDAV client, attempting to discover calendars...");
|
|
|
|
// Try to discover calendars as an authentication test
|
|
match caldav_client.discover_calendars().await {
|
|
Ok(calendars) => {
|
|
println!("✅ Authentication successful! Found {} calendars", calendars.len());
|
|
// Authentication successful, generate JWT token
|
|
let token = self.generate_token(&request.username, &request.server_url)?;
|
|
|
|
Ok(AuthResponse {
|
|
token,
|
|
username: request.username,
|
|
server_url: request.server_url,
|
|
})
|
|
}
|
|
Err(err) => {
|
|
println!("❌ Authentication failed: {:?}", err);
|
|
// Authentication failed
|
|
Err(ApiError::Unauthorized("Invalid CalDAV credentials or server unavailable".to_string()))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Verify JWT token and extract CalDAV credentials info
|
|
pub fn verify_token(&self, token: &str) -> Result<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,
|
|
})
|
|
}
|
|
|
|
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<String, ApiError> {
|
|
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<Claims, ApiError> {
|
|
let token_data = decode::<Claims>(
|
|
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)
|
|
}
|
|
} |