use chrono::{DateTime, Duration, Utc}; use serde::{Deserialize, Serialize}; use sqlx::sqlite::{SqlitePool, SqlitePoolOptions}; use sqlx::{FromRow, Result}; use std::sync::Arc; use uuid::Uuid; /// Database connection pool wrapper #[derive(Clone)] pub struct Database { pool: Arc, } impl Database { /// Create a new database connection pool pub async fn new(database_url: &str) -> Result { let pool = SqlitePoolOptions::new() .max_connections(5) .connect(database_url) .await?; // Run migrations sqlx::migrate!("./migrations").run(&pool).await?; Ok(Self { pool: Arc::new(pool), }) } /// Get a reference to the connection pool pub fn pool(&self) -> &SqlitePool { &self.pool } } /// User model representing a CalDAV user #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct User { pub id: String, // UUID as string for SQLite pub username: String, pub server_url: String, pub created_at: DateTime, } impl User { /// Create a new user with generated UUID pub fn new(username: String, server_url: String) -> Self { Self { id: Uuid::new_v4().to_string(), username, server_url, created_at: Utc::now(), } } } /// Session model for user sessions #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct Session { pub id: String, // UUID as string pub user_id: String, // Foreign key to User pub token: String, // Session token pub created_at: DateTime, pub expires_at: DateTime, pub last_accessed: DateTime, } impl Session { /// Create a new session for a user pub fn new(user_id: String, token: String, expires_in_hours: i64) -> Self { let now = Utc::now(); Self { id: Uuid::new_v4().to_string(), user_id, token, created_at: now, expires_at: now + chrono::Duration::hours(expires_in_hours), last_accessed: now, } } /// Check if the session has expired pub fn is_expired(&self) -> bool { Utc::now() > self.expires_at } } /// User preferences model #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct UserPreferences { pub user_id: String, pub calendar_selected_date: Option, pub calendar_time_increment: Option, pub calendar_view_mode: Option, pub calendar_theme: Option, pub calendar_style: Option, pub calendar_colors: Option, // JSON string pub last_used_calendar: Option, pub updated_at: DateTime, } /// External calendar model #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct ExternalCalendar { pub id: i32, pub user_id: String, pub name: String, pub url: String, pub color: String, pub is_visible: bool, pub created_at: DateTime, pub updated_at: DateTime, pub last_fetched: Option>, } impl ExternalCalendar { /// Create a new external calendar pub fn new(user_id: String, name: String, url: String, color: String) -> Self { let now = Utc::now(); Self { id: 0, // Will be set by database user_id, name, url, color, is_visible: true, created_at: now, updated_at: now, last_fetched: None, } } } impl UserPreferences { /// Create default preferences for a new user pub fn default_for_user(user_id: String) -> Self { Self { user_id, calendar_selected_date: None, calendar_time_increment: Some(15), calendar_view_mode: Some("month".to_string()), calendar_theme: Some("light".to_string()), calendar_style: Some("default".to_string()), calendar_colors: None, last_used_calendar: None, updated_at: Utc::now(), } } } /// Repository for User operations pub struct UserRepository<'a> { db: &'a Database, } impl<'a> UserRepository<'a> { pub fn new(db: &'a Database) -> Self { Self { db } } /// Find or create a user by username and server URL pub async fn find_or_create( &self, username: &str, server_url: &str, ) -> Result { // Try to find existing user let existing = sqlx::query_as::<_, User>( "SELECT * FROM users WHERE username = ? AND server_url = ?", ) .bind(username) .bind(server_url) .fetch_optional(self.db.pool()) .await?; if let Some(user) = existing { Ok(user) } else { // Create new user let user = User::new(username.to_string(), server_url.to_string()); sqlx::query( "INSERT INTO users (id, username, server_url, created_at) VALUES (?, ?, ?, ?)", ) .bind(&user.id) .bind(&user.username) .bind(&user.server_url) .bind(&user.created_at) .execute(self.db.pool()) .await?; Ok(user) } } /// Find a user by ID pub async fn find_by_id(&self, user_id: &str) -> Result> { sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = ?") .bind(user_id) .fetch_optional(self.db.pool()) .await } } /// Repository for Session operations pub struct SessionRepository<'a> { db: &'a Database, } impl<'a> SessionRepository<'a> { pub fn new(db: &'a Database) -> Self { Self { db } } /// Create a new session pub async fn create(&self, session: &Session) -> Result<()> { sqlx::query( "INSERT INTO sessions (id, user_id, token, created_at, expires_at, last_accessed) VALUES (?, ?, ?, ?, ?, ?)", ) .bind(&session.id) .bind(&session.user_id) .bind(&session.token) .bind(&session.created_at) .bind(&session.expires_at) .bind(&session.last_accessed) .execute(self.db.pool()) .await?; Ok(()) } /// Find a session by token and update last_accessed pub async fn find_by_token(&self, token: &str) -> Result> { let session = sqlx::query_as::<_, Session>("SELECT * FROM sessions WHERE token = ?") .bind(token) .fetch_optional(self.db.pool()) .await?; if let Some(ref s) = session { if !s.is_expired() { // Update last_accessed time sqlx::query("UPDATE sessions SET last_accessed = ? WHERE id = ?") .bind(Utc::now()) .bind(&s.id) .execute(self.db.pool()) .await?; } } Ok(session) } /// Delete a session (logout) pub async fn delete(&self, token: &str) -> Result<()> { sqlx::query("DELETE FROM sessions WHERE token = ?") .bind(token) .execute(self.db.pool()) .await?; Ok(()) } /// Clean up expired sessions pub async fn cleanup_expired(&self) -> Result { let result = sqlx::query("DELETE FROM sessions WHERE expires_at < ?") .bind(Utc::now()) .execute(self.db.pool()) .await?; Ok(result.rows_affected()) } } /// Repository for UserPreferences operations pub struct PreferencesRepository<'a> { db: &'a Database, } impl<'a> PreferencesRepository<'a> { pub fn new(db: &'a Database) -> Self { Self { db } } /// Get user preferences, creating defaults if not exist pub async fn get_or_create(&self, user_id: &str) -> Result { let existing = sqlx::query_as::<_, UserPreferences>( "SELECT * FROM user_preferences WHERE user_id = ?", ) .bind(user_id) .fetch_optional(self.db.pool()) .await?; if let Some(prefs) = existing { Ok(prefs) } else { // Create default preferences let prefs = UserPreferences::default_for_user(user_id.to_string()); sqlx::query( "INSERT INTO user_preferences (user_id, calendar_selected_date, calendar_time_increment, calendar_view_mode, calendar_theme, calendar_style, calendar_colors, last_used_calendar, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", ) .bind(&prefs.user_id) .bind(&prefs.calendar_selected_date) .bind(&prefs.calendar_time_increment) .bind(&prefs.calendar_view_mode) .bind(&prefs.calendar_theme) .bind(&prefs.calendar_style) .bind(&prefs.calendar_colors) .bind(&prefs.last_used_calendar) .bind(&prefs.updated_at) .execute(self.db.pool()) .await?; Ok(prefs) } } /// Update user preferences pub async fn update(&self, prefs: &UserPreferences) -> Result<()> { sqlx::query( "UPDATE user_preferences SET calendar_selected_date = ?, calendar_time_increment = ?, calendar_view_mode = ?, calendar_theme = ?, calendar_style = ?, calendar_colors = ?, last_used_calendar = ?, updated_at = ? WHERE user_id = ?", ) .bind(&prefs.calendar_selected_date) .bind(&prefs.calendar_time_increment) .bind(&prefs.calendar_view_mode) .bind(&prefs.calendar_theme) .bind(&prefs.calendar_style) .bind(&prefs.calendar_colors) .bind(&prefs.last_used_calendar) .bind(Utc::now()) .bind(&prefs.user_id) .execute(self.db.pool()) .await?; Ok(()) } } /// Repository for ExternalCalendar operations pub struct ExternalCalendarRepository<'a> { db: &'a Database, } impl<'a> ExternalCalendarRepository<'a> { pub fn new(db: &'a Database) -> Self { Self { db } } /// Get all external calendars for a user pub async fn get_by_user(&self, user_id: &str) -> Result> { sqlx::query_as::<_, ExternalCalendar>( "SELECT * FROM external_calendars WHERE user_id = ? ORDER BY created_at ASC", ) .bind(user_id) .fetch_all(self.db.pool()) .await } /// Create a new external calendar pub async fn create(&self, calendar: &ExternalCalendar) -> Result { let result = sqlx::query( "INSERT INTO external_calendars (user_id, name, url, color, is_visible, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)", ) .bind(&calendar.user_id) .bind(&calendar.name) .bind(&calendar.url) .bind(&calendar.color) .bind(&calendar.is_visible) .bind(&calendar.created_at) .bind(&calendar.updated_at) .execute(self.db.pool()) .await?; Ok(result.last_insert_rowid() as i32) } /// Update an external calendar pub async fn update(&self, id: i32, calendar: &ExternalCalendar) -> Result<()> { sqlx::query( "UPDATE external_calendars SET name = ?, url = ?, color = ?, is_visible = ?, updated_at = ? WHERE id = ? AND user_id = ?", ) .bind(&calendar.name) .bind(&calendar.url) .bind(&calendar.color) .bind(&calendar.is_visible) .bind(Utc::now()) .bind(id) .bind(&calendar.user_id) .execute(self.db.pool()) .await?; Ok(()) } /// Delete an external calendar pub async fn delete(&self, id: i32, user_id: &str) -> Result<()> { sqlx::query("DELETE FROM external_calendars WHERE id = ? AND user_id = ?") .bind(id) .bind(user_id) .execute(self.db.pool()) .await?; Ok(()) } /// Update last_fetched timestamp pub async fn update_last_fetched(&self, id: i32, user_id: &str) -> Result<()> { sqlx::query( "UPDATE external_calendars SET last_fetched = ? WHERE id = ? AND user_id = ?", ) .bind(Utc::now()) .bind(id) .bind(user_id) .execute(self.db.pool()) .await?; Ok(()) } /// Get cached ICS data for an external calendar pub async fn get_cached_data(&self, external_calendar_id: i32) -> Result)>> { let result = sqlx::query_as::<_, (String, DateTime)>( "SELECT ics_data, cached_at FROM external_calendar_cache WHERE external_calendar_id = ?", ) .bind(external_calendar_id) .fetch_optional(self.db.pool()) .await?; Ok(result) } /// Update cache with new ICS data pub async fn update_cache(&self, external_calendar_id: i32, ics_data: &str, etag: Option<&str>) -> Result<()> { sqlx::query( "INSERT INTO external_calendar_cache (external_calendar_id, ics_data, etag, cached_at) VALUES (?, ?, ?, ?) ON CONFLICT(external_calendar_id) DO UPDATE SET ics_data = excluded.ics_data, etag = excluded.etag, cached_at = excluded.cached_at", ) .bind(external_calendar_id) .bind(ics_data) .bind(etag) .bind(Utc::now()) .execute(self.db.pool()) .await?; Ok(()) } /// Check if cache is stale (older than max_age_minutes) pub async fn is_cache_stale(&self, external_calendar_id: i32, max_age_minutes: i64) -> Result { let cutoff_time = Utc::now() - Duration::minutes(max_age_minutes); let result = sqlx::query_scalar::<_, i64>( "SELECT COUNT(*) FROM external_calendar_cache WHERE external_calendar_id = ? AND cached_at > ?", ) .bind(external_calendar_id) .bind(cutoff_time) .fetch_one(self.db.pool()) .await?; Ok(result == 0) } /// Clear cache for an external calendar pub async fn clear_cache(&self, external_calendar_id: i32) -> Result<()> { sqlx::query("DELETE FROM external_calendar_cache WHERE external_calendar_id = ?") .bind(external_calendar_id) .execute(self.db.pool()) .await?; Ok(()) } }