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:
@@ -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<AuthResponse, ApiError> {
|
||||
/// Authenticate user directly against CalDAV server
|
||||
pub async fn login(&self, request: CalDAVLoginRequest) -> Result<AuthResponse, ApiError> {
|
||||
// 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<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?;
|
||||
// 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<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> {
|
||||
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> {
|
||||
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<String, ApiError> {
|
||||
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 {
|
||||
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(
|
||||
|
||||
@@ -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("</d:response>") {
|
||||
let response_content = &response_block[..end_pos];
|
||||
|
||||
// Look for actual calendar collections (not just containers)
|
||||
if response_content.contains("<c:supported-calendar-component-set") ||
|
||||
(response_content.contains("<d:collection/>") &&
|
||||
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("<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);
|
||||
} 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 chrono::Datelike;
|
||||
|
||||
use crate::{AppState, models::{LoginRequest, RegisterRequest, AuthResponse, ApiError}};
|
||||
use crate::{AppState, models::{CalDAVLoginRequest, AuthResponse, ApiError}};
|
||||
use crate::calendar::{CalDAVClient, CalendarEvent};
|
||||
use crate::config::CalDAVConfig;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CalendarQuery {
|
||||
@@ -18,31 +17,17 @@ pub struct CalendarQuery {
|
||||
}
|
||||
|
||||
pub async fn get_calendar_events(
|
||||
State(_state): State<Arc<AppState>>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(params): Query<CalendarQuery>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<Json<Vec<CalendarEvent>>, 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<Arc<AppState>>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(uid): Path<String>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<Json<Option<CalendarEvent>>, 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<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(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(request): Json<LoginRequest>,
|
||||
Json(request): Json<CalDAVLoginRequest>,
|
||||
) -> 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?;
|
||||
Ok(Json(response))
|
||||
}
|
||||
@@ -139,25 +106,40 @@ pub async fn verify_token(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<Json<serde_json::Value>, 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<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},
|
||||
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<dyn std::error::Error>> {
|
||||
// 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<dyn std::error::Error>> {
|
||||
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))
|
||||
|
||||
@@ -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<Utc>,
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user