Added SQLite database for session management and user preferences storage, allowing users to have consistent settings across different sessions and devices. Backend changes: - Added SQLite database with users, sessions, and preferences tables - Implemented session-based authentication alongside JWT tokens - Created preference storage/retrieval API endpoints - Database migrations for schema setup - Session validation and cleanup functionality Frontend changes: - Added "Remember server" and "Remember username" checkboxes to login - Created preferences service for syncing settings with backend - Updated auth flow to handle session tokens and preferences - Store remembered values in LocalStorage (not database) for convenience Key features: - User preferences persist across sessions and devices - CalDAV passwords never stored, only passed through - Sessions expire after 24 hours - Remember checkboxes only affect local browser storage 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
298 lines
10 KiB
Rust
298 lines
10 KiB
Rust
use axum::{
|
|
http::StatusCode,
|
|
response::{IntoResponse, Response},
|
|
Json,
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
// API request/response types
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct CalDAVLoginRequest {
|
|
pub username: String,
|
|
pub password: String,
|
|
pub server_url: String,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct AuthResponse {
|
|
pub token: String,
|
|
pub session_token: String,
|
|
pub username: String,
|
|
pub server_url: String,
|
|
pub preferences: UserPreferencesResponse,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
pub struct UserPreferencesResponse {
|
|
pub calendar_selected_date: Option<String>,
|
|
pub calendar_time_increment: Option<i32>,
|
|
pub calendar_view_mode: Option<String>,
|
|
pub calendar_theme: Option<String>,
|
|
pub calendar_colors: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct UpdatePreferencesRequest {
|
|
pub calendar_selected_date: Option<String>,
|
|
pub calendar_time_increment: Option<i32>,
|
|
pub calendar_view_mode: Option<String>,
|
|
pub calendar_theme: Option<String>,
|
|
pub calendar_colors: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct UserInfo {
|
|
pub username: String,
|
|
pub server_url: String,
|
|
pub calendars: Vec<CalendarInfo>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct CalendarInfo {
|
|
pub path: String,
|
|
pub display_name: String,
|
|
pub color: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct CreateCalendarRequest {
|
|
pub name: String,
|
|
pub description: Option<String>,
|
|
pub color: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct CreateCalendarResponse {
|
|
pub success: bool,
|
|
pub message: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct DeleteCalendarRequest {
|
|
pub path: String,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct DeleteCalendarResponse {
|
|
pub success: bool,
|
|
pub message: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct DeleteEventRequest {
|
|
pub calendar_path: String,
|
|
pub event_href: String,
|
|
pub delete_action: String, // "delete_this", "delete_following", or "delete_series"
|
|
pub occurrence_date: Option<String>, // ISO date string for the specific occurrence
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct DeleteEventResponse {
|
|
pub success: bool,
|
|
pub message: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct CreateEventRequest {
|
|
pub title: String,
|
|
pub description: String,
|
|
pub start_date: String, // YYYY-MM-DD format
|
|
pub start_time: String, // HH:MM format
|
|
pub end_date: String, // YYYY-MM-DD format
|
|
pub end_time: String, // HH:MM format
|
|
pub location: String,
|
|
pub all_day: bool,
|
|
pub status: String, // confirmed, tentative, cancelled
|
|
pub class: String, // public, private, confidential
|
|
pub priority: Option<u8>, // 0-9 priority level
|
|
pub organizer: String, // organizer email
|
|
pub attendees: String, // comma-separated attendee emails
|
|
pub categories: String, // comma-separated categories
|
|
pub reminder: String, // reminder type
|
|
pub recurrence: String, // recurrence type
|
|
pub recurrence_days: Vec<bool>, // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] for weekly recurrence
|
|
pub calendar_path: Option<String>, // Optional - use first calendar if not specified
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct CreateEventResponse {
|
|
pub success: bool,
|
|
pub message: String,
|
|
pub event_href: Option<String>, // The created event's href/filename
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct UpdateEventRequest {
|
|
pub uid: String, // Event UID to identify which event to update
|
|
pub title: String,
|
|
pub description: String,
|
|
pub start_date: String, // YYYY-MM-DD format
|
|
pub start_time: String, // HH:MM format
|
|
pub end_date: String, // YYYY-MM-DD format
|
|
pub end_time: String, // HH:MM format
|
|
pub location: String,
|
|
pub all_day: bool,
|
|
pub status: String, // confirmed, tentative, cancelled
|
|
pub class: String, // public, private, confidential
|
|
pub priority: Option<u8>, // 0-9 priority level
|
|
pub organizer: String, // organizer email
|
|
pub attendees: String, // comma-separated attendee emails
|
|
pub categories: String, // comma-separated categories
|
|
pub reminder: String, // reminder type
|
|
pub recurrence: String, // recurrence type
|
|
pub recurrence_days: Vec<bool>, // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] for weekly recurrence
|
|
pub calendar_path: Option<String>, // Optional - search all calendars if not specified
|
|
pub update_action: Option<String>, // "update_series" for recurring events
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub until_date: Option<String>, // ISO datetime string for RRULE UNTIL clause
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct UpdateEventResponse {
|
|
pub success: bool,
|
|
pub message: String,
|
|
}
|
|
|
|
// ==================== EVENT SERIES MODELS ====================
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct CreateEventSeriesRequest {
|
|
pub title: String,
|
|
pub description: String,
|
|
pub start_date: String, // YYYY-MM-DD format
|
|
pub start_time: String, // HH:MM format
|
|
pub end_date: String, // YYYY-MM-DD format
|
|
pub end_time: String, // HH:MM format
|
|
pub location: String,
|
|
pub all_day: bool,
|
|
pub status: String, // confirmed, tentative, cancelled
|
|
pub class: String, // public, private, confidential
|
|
pub priority: Option<u8>, // 0-9 priority level
|
|
pub organizer: String, // organizer email
|
|
pub attendees: String, // comma-separated attendee emails
|
|
pub categories: String, // comma-separated categories
|
|
pub reminder: String, // reminder type
|
|
|
|
// Series-specific fields
|
|
pub recurrence: String, // recurrence type (daily, weekly, monthly, yearly)
|
|
pub recurrence_days: Vec<bool>, // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] for weekly recurrence
|
|
pub recurrence_interval: Option<u32>, // Every N days/weeks/months/years
|
|
pub recurrence_end_date: Option<String>, // When the series ends (YYYY-MM-DD)
|
|
pub recurrence_count: Option<u32>, // Number of occurrences
|
|
pub calendar_path: Option<String>, // Optional - search all calendars if not specified
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct CreateEventSeriesResponse {
|
|
pub success: bool,
|
|
pub message: String,
|
|
pub series_uid: Option<String>, // The base UID for the series
|
|
pub occurrences_created: Option<u32>, // Number of individual events created
|
|
pub event_href: Option<String>, // The created series' href/filename
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct UpdateEventSeriesRequest {
|
|
pub series_uid: String, // Series UID to identify which series to update
|
|
pub title: String,
|
|
pub description: String,
|
|
pub start_date: String, // YYYY-MM-DD format
|
|
pub start_time: String, // HH:MM format
|
|
pub end_date: String, // YYYY-MM-DD format
|
|
pub end_time: String, // HH:MM format
|
|
pub location: String,
|
|
pub all_day: bool,
|
|
pub status: String, // confirmed, tentative, cancelled
|
|
pub class: String, // public, private, confidential
|
|
pub priority: Option<u8>, // 0-9 priority level
|
|
pub organizer: String, // organizer email
|
|
pub attendees: String, // comma-separated attendee emails
|
|
pub categories: String, // comma-separated categories
|
|
pub reminder: String, // reminder type
|
|
|
|
// Series-specific fields
|
|
pub recurrence: String, // recurrence type (daily, weekly, monthly, yearly)
|
|
pub recurrence_days: Vec<bool>, // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] for weekly recurrence
|
|
pub recurrence_interval: Option<u32>, // Every N days/weeks/months/years
|
|
pub recurrence_end_date: Option<String>, // When the series ends (YYYY-MM-DD)
|
|
pub recurrence_count: Option<u32>, // Number of occurrences
|
|
pub calendar_path: Option<String>, // Optional - search all calendars if not specified
|
|
|
|
// Update scope control
|
|
pub update_scope: String, // "this_only", "this_and_future", "all_in_series"
|
|
pub occurrence_date: Option<String>, // ISO date string for specific occurrence being updated
|
|
pub changed_fields: Option<Vec<String>>, // List of field names that were changed (for optimization)
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct UpdateEventSeriesResponse {
|
|
pub success: bool,
|
|
pub message: String,
|
|
pub series_uid: Option<String>,
|
|
pub occurrences_affected: Option<u32>, // Number of events updated
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct DeleteEventSeriesRequest {
|
|
pub series_uid: String, // Series UID to identify which series to delete
|
|
pub calendar_path: String,
|
|
pub event_href: String,
|
|
|
|
// Delete scope control
|
|
pub delete_scope: String, // "this_only", "this_and_future", "all_in_series"
|
|
pub occurrence_date: Option<String>, // ISO date string for specific occurrence being deleted
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct DeleteEventSeriesResponse {
|
|
pub success: bool,
|
|
pub message: String,
|
|
pub occurrences_affected: Option<u32>, // Number of events deleted
|
|
}
|
|
|
|
// Error handling
|
|
#[derive(Debug)]
|
|
pub enum ApiError {
|
|
Database(String),
|
|
NotFound(String),
|
|
Unauthorized(String),
|
|
BadRequest(String),
|
|
Conflict(String),
|
|
Internal(String),
|
|
}
|
|
|
|
impl IntoResponse for ApiError {
|
|
fn into_response(self) -> Response {
|
|
let (status, error_message) = match self {
|
|
ApiError::Database(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),
|
|
ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
|
|
ApiError::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, msg),
|
|
ApiError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
|
|
ApiError::Conflict(msg) => (StatusCode::CONFLICT, msg),
|
|
ApiError::Internal(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),
|
|
};
|
|
|
|
let body = Json(serde_json::json!({
|
|
"error": error_message,
|
|
"status": status.as_u16()
|
|
}));
|
|
|
|
(status, body).into_response()
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for ApiError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
ApiError::Database(msg) => write!(f, "Database error: {}", msg),
|
|
ApiError::NotFound(msg) => write!(f, "Not found: {}", msg),
|
|
ApiError::Unauthorized(msg) => write!(f, "Unauthorized: {}", msg),
|
|
ApiError::BadRequest(msg) => write!(f, "Bad request: {}", msg),
|
|
ApiError::Conflict(msg) => write!(f, "Conflict: {}", msg),
|
|
ApiError::Internal(msg) => write!(f, "Internal error: {}", msg),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for ApiError {}
|