Files
calendar/backend/src/models.rs
Connor Johnstone 03c0011445 Implement lightweight auth system with SQLite
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>
2025-09-01 18:55:09 -04:00

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