Files
calendar/frontend/src/services/preferences.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

177 lines
6.8 KiB
Rust

use gloo_storage::{LocalStorage, Storage};
use serde::{Deserialize, Serialize};
use serde_json;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
use web_sys::{Request, RequestInit, RequestMode, Response};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UserPreferences {
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 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>,
}
pub struct PreferencesService {
base_url: String,
}
impl PreferencesService {
pub fn new() -> Self {
let base_url = option_env!("BACKEND_API_URL")
.unwrap_or("http://localhost:3000/api")
.to_string();
Self { base_url }
}
/// Load preferences from LocalStorage (cached from login)
pub fn load_cached() -> Option<UserPreferences> {
if let Ok(prefs_json) = LocalStorage::get::<String>("user_preferences") {
serde_json::from_str(&prefs_json).ok()
} else {
None
}
}
/// Update a single preference field and sync with backend
pub async fn update_preference(&self, field: &str, value: serde_json::Value) -> Result<(), String> {
// Get session token
let session_token = LocalStorage::get::<String>("session_token")
.map_err(|_| "No session token found".to_string())?;
// Load current preferences
let mut preferences = Self::load_cached().unwrap_or(UserPreferences {
calendar_selected_date: None,
calendar_time_increment: None,
calendar_view_mode: None,
calendar_theme: None,
calendar_colors: None,
});
// Update the specific field
match field {
"calendar_selected_date" => {
preferences.calendar_selected_date = value.as_str().map(|s| s.to_string());
}
"calendar_time_increment" => {
preferences.calendar_time_increment = value.as_i64().map(|i| i as i32);
}
"calendar_view_mode" => {
preferences.calendar_view_mode = value.as_str().map(|s| s.to_string());
}
"calendar_theme" => {
preferences.calendar_theme = value.as_str().map(|s| s.to_string());
}
"calendar_colors" => {
preferences.calendar_colors = value.as_str().map(|s| s.to_string());
}
_ => return Err(format!("Unknown preference field: {}", field)),
}
// Save to LocalStorage cache
if let Ok(prefs_json) = serde_json::to_string(&preferences) {
let _ = LocalStorage::set("user_preferences", &prefs_json);
}
// Sync with backend
let request = UpdatePreferencesRequest {
calendar_selected_date: preferences.calendar_selected_date.clone(),
calendar_time_increment: preferences.calendar_time_increment,
calendar_view_mode: preferences.calendar_view_mode.clone(),
calendar_theme: preferences.calendar_theme.clone(),
calendar_colors: preferences.calendar_colors.clone(),
};
self.sync_preferences(&session_token, &request).await
}
/// Sync all preferences with backend
async fn sync_preferences(
&self,
session_token: &str,
request: &UpdatePreferencesRequest,
) -> Result<(), String> {
let window = web_sys::window().ok_or("No global window exists")?;
let json_body = serde_json::to_string(request)
.map_err(|e| format!("JSON serialization failed: {}", e))?;
let opts = RequestInit::new();
opts.set_method("POST");
opts.set_mode(RequestMode::Cors);
opts.set_body(&wasm_bindgen::JsValue::from_str(&json_body));
let url = format!("{}/preferences", self.base_url);
let request = Request::new_with_str_and_init(&url, &opts)
.map_err(|e| format!("Request creation failed: {:?}", e))?;
request
.headers()
.set("Content-Type", "application/json")
.map_err(|e| format!("Header setting failed: {:?}", e))?;
request
.headers()
.set("X-Session-Token", session_token)
.map_err(|e| format!("Header setting failed: {:?}", e))?;
let resp_value = JsFuture::from(window.fetch_with_request(&request))
.await
.map_err(|e| format!("Network request failed: {:?}", e))?;
let resp: Response = resp_value
.dyn_into()
.map_err(|e| format!("Response cast failed: {:?}", e))?;
if resp.ok() {
Ok(())
} else {
Err(format!("Failed to update preferences: {}", resp.status()))
}
}
/// Migrate preferences from LocalStorage to backend (on first login after update)
pub async fn migrate_from_local_storage(&self) -> Result<(), String> {
let session_token = LocalStorage::get::<String>("session_token")
.map_err(|_| "No session token found".to_string())?;
let mut request = UpdatePreferencesRequest {
calendar_selected_date: LocalStorage::get::<String>("calendar_selected_date").ok(),
calendar_time_increment: LocalStorage::get::<u32>("calendar_time_increment").ok().map(|i| i as i32),
calendar_view_mode: LocalStorage::get::<String>("calendar_view_mode").ok(),
calendar_theme: LocalStorage::get::<String>("calendar_theme").ok(),
calendar_colors: LocalStorage::get::<String>("calendar_colors").ok(),
};
// Only migrate if we have some preferences to migrate
if request.calendar_selected_date.is_some()
|| request.calendar_time_increment.is_some()
|| request.calendar_view_mode.is_some()
|| request.calendar_theme.is_some()
|| request.calendar_colors.is_some()
{
self.sync_preferences(&session_token, &request).await?;
// Clear old LocalStorage entries after successful migration
let _ = LocalStorage::delete("calendar_selected_date");
let _ = LocalStorage::delete("calendar_time_increment");
let _ = LocalStorage::delete("calendar_view_mode");
let _ = LocalStorage::delete("calendar_theme");
let _ = LocalStorage::delete("calendar_colors");
}
Ok(())
}
}