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

111 lines
3.5 KiB
Rust

// Frontend authentication module - connects to backend API
use serde::{Deserialize, Serialize};
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
use web_sys::{Request, RequestInit, RequestMode, Response};
#[derive(Debug, Serialize, Deserialize)]
pub struct CalDAVLoginRequest {
pub server_url: String,
pub username: String,
pub password: String,
}
#[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, Serialize, Deserialize)]
pub struct AuthResponse {
pub token: String,
pub session_token: String,
pub username: String,
pub server_url: String,
pub preferences: UserPreferencesResponse,
}
#[derive(Debug, Deserialize)]
pub struct ApiErrorResponse {
pub error: String,
}
// Frontend auth service - connects to backend API
pub struct AuthService {
base_url: String,
}
impl AuthService {
pub fn new() -> Self {
// Get backend URL from environment variable at compile time, fallback to localhost
let base_url = option_env!("BACKEND_API_URL")
.unwrap_or("http://localhost:3000/api")
.to_string();
Self { base_url }
}
pub async fn login(&self, request: CalDAVLoginRequest) -> Result<AuthResponse, String> {
self.post_json("/auth/login", &request).await
}
// Helper method for POST requests with JSON body
async fn post_json<T: Serialize, R: for<'de> Deserialize<'de>>(
&self,
endpoint: &str,
body: &T,
) -> Result<R, String> {
let window = web_sys::window().ok_or("No global window exists")?;
let json_body =
serde_json::to_string(body).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!("{}{}", self.base_url, endpoint);
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))?;
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))?;
let text = JsFuture::from(
resp.text()
.map_err(|e| format!("Text extraction failed: {:?}", e))?,
)
.await
.map_err(|e| format!("Text promise failed: {:?}", e))?;
let text_string = text.as_string().ok_or("Response text is not a string")?;
if resp.ok() {
serde_json::from_str::<R>(&text_string)
.map_err(|e| format!("JSON parsing failed: {}", e))
} else {
// Try to parse error response
if let Ok(error_response) = serde_json::from_str::<ApiErrorResponse>(&text_string) {
Err(error_response.error)
} else {
Err(format!("Request failed with status {}", resp.status()))
}
}
}
}