 03c0011445
			
		
	
	03c0011445
	
	
	
		
			
			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>
		
			
				
	
	
		
			305 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			305 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| use chrono::{DateTime, 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<SqlitePool>,
 | |
| }
 | |
| 
 | |
| impl Database {
 | |
|     /// Create a new database connection pool
 | |
|     pub async fn new(database_url: &str) -> Result<Self> {
 | |
|         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<Utc>,
 | |
| }
 | |
| 
 | |
| 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<Utc>,
 | |
|     pub expires_at: DateTime<Utc>,
 | |
|     pub last_accessed: DateTime<Utc>,
 | |
| }
 | |
| 
 | |
| 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<String>,
 | |
|     pub calendar_time_increment: Option<i32>,
 | |
|     pub calendar_view_mode: Option<String>,
 | |
|     pub calendar_theme: Option<String>,
 | |
|     pub calendar_colors: Option<String>, // JSON string
 | |
|     pub updated_at: DateTime<Utc>,
 | |
| }
 | |
| 
 | |
| 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_colors: 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<User> {
 | |
|         // 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<Option<User>> {
 | |
|         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<Option<Session>> {
 | |
|         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<u64> {
 | |
|         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<UserPreferences> {
 | |
|         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_colors, 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_colors)
 | |
|             .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_colors = ?, 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_colors)
 | |
|         .bind(Utc::now())
 | |
|         .bind(&prefs.user_id)
 | |
|         .execute(self.db.pool())
 | |
|         .await?;
 | |
| 
 | |
|         Ok(())
 | |
|     }
 | |
| } |