Files
calendar/backend/src/auth.rs
Connor Johnstone 78f1db7203 Refactor handlers.rs into modular structure for better maintainability
- 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>
2025-08-30 13:35:13 -04:00

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)
}
}