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>
This commit is contained in:
@@ -9,11 +9,24 @@ pub struct LoginProps {
|
||||
|
||||
#[function_component]
|
||||
pub fn Login(props: &LoginProps) -> Html {
|
||||
let server_url = use_state(String::new);
|
||||
let username = use_state(String::new);
|
||||
// Load remembered values from LocalStorage on mount
|
||||
let server_url = use_state(|| {
|
||||
LocalStorage::get::<String>("remembered_server_url").unwrap_or_default()
|
||||
});
|
||||
let username = use_state(|| {
|
||||
LocalStorage::get::<String>("remembered_username").unwrap_or_default()
|
||||
});
|
||||
let password = use_state(String::new);
|
||||
let error_message = use_state(|| Option::<String>::None);
|
||||
let is_loading = use_state(|| false);
|
||||
|
||||
// Remember checkboxes state
|
||||
let remember_server = use_state(|| {
|
||||
LocalStorage::get::<String>("remembered_server_url").is_ok()
|
||||
});
|
||||
let remember_username = use_state(|| {
|
||||
LocalStorage::get::<String>("remembered_username").is_ok()
|
||||
});
|
||||
|
||||
let server_url_ref = use_node_ref();
|
||||
let username_ref = use_node_ref();
|
||||
@@ -42,6 +55,38 @@ pub fn Login(props: &LoginProps) -> Html {
|
||||
password.set(target.value());
|
||||
})
|
||||
};
|
||||
|
||||
let on_remember_server_change = {
|
||||
let remember_server = remember_server.clone();
|
||||
let server_url = server_url.clone();
|
||||
Callback::from(move |e: Event| {
|
||||
let target = e.target_unchecked_into::<HtmlInputElement>();
|
||||
let checked = target.checked();
|
||||
remember_server.set(checked);
|
||||
|
||||
if checked {
|
||||
let _ = LocalStorage::set("remembered_server_url", (*server_url).clone());
|
||||
} else {
|
||||
let _ = LocalStorage::delete("remembered_server_url");
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let on_remember_username_change = {
|
||||
let remember_username = remember_username.clone();
|
||||
let username = username.clone();
|
||||
Callback::from(move |e: Event| {
|
||||
let target = e.target_unchecked_into::<HtmlInputElement>();
|
||||
let checked = target.checked();
|
||||
remember_username.set(checked);
|
||||
|
||||
if checked {
|
||||
let _ = LocalStorage::set("remembered_username", (*username).clone());
|
||||
} else {
|
||||
let _ = LocalStorage::delete("remembered_username");
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let on_submit = {
|
||||
let server_url = server_url.clone();
|
||||
@@ -73,7 +118,7 @@ pub fn Login(props: &LoginProps) -> Html {
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
web_sys::console::log_1(&"🚀 Starting login process...".into());
|
||||
match perform_login(server_url.clone(), username.clone(), password.clone()).await {
|
||||
Ok((token, credentials)) => {
|
||||
Ok((token, session_token, credentials, preferences)) => {
|
||||
web_sys::console::log_1(&"✅ Login successful!".into());
|
||||
// Store token and credentials in local storage
|
||||
if let Err(_) = LocalStorage::set("auth_token", &token) {
|
||||
@@ -82,11 +127,22 @@ pub fn Login(props: &LoginProps) -> Html {
|
||||
is_loading.set(false);
|
||||
return;
|
||||
}
|
||||
if let Err(_) = LocalStorage::set("session_token", &session_token) {
|
||||
error_message
|
||||
.set(Some("Failed to store session token".to_string()));
|
||||
is_loading.set(false);
|
||||
return;
|
||||
}
|
||||
if let Err(_) = LocalStorage::set("caldav_credentials", &credentials) {
|
||||
error_message.set(Some("Failed to store credentials".to_string()));
|
||||
is_loading.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Store preferences from database
|
||||
if let Ok(prefs_json) = serde_json::to_string(&preferences) {
|
||||
let _ = LocalStorage::set("user_preferences", &prefs_json);
|
||||
}
|
||||
|
||||
is_loading.set(false);
|
||||
on_login.emit(token);
|
||||
@@ -117,6 +173,15 @@ pub fn Login(props: &LoginProps) -> Html {
|
||||
onchange={on_server_url_change}
|
||||
disabled={*is_loading}
|
||||
/>
|
||||
<div class="remember-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="remember_server"
|
||||
checked={*remember_server}
|
||||
onchange={on_remember_server_change}
|
||||
/>
|
||||
<label for="remember_server">{"Remember server"}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@@ -130,6 +195,15 @@ pub fn Login(props: &LoginProps) -> Html {
|
||||
onchange={on_username_change}
|
||||
disabled={*is_loading}
|
||||
/>
|
||||
<div class="remember-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="remember_username"
|
||||
checked={*remember_username}
|
||||
onchange={on_remember_username_change}
|
||||
/>
|
||||
<label for="remember_username">{"Remember username"}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@@ -177,7 +251,7 @@ async fn perform_login(
|
||||
server_url: String,
|
||||
username: String,
|
||||
password: String,
|
||||
) -> Result<(String, String), String> {
|
||||
) -> Result<(String, String, String, serde_json::Value), String> {
|
||||
use crate::auth::{AuthService, CalDAVLoginRequest};
|
||||
use serde_json;
|
||||
|
||||
@@ -201,7 +275,17 @@ async fn perform_login(
|
||||
"username": username,
|
||||
"password": password
|
||||
});
|
||||
Ok((response.token, credentials.to_string()))
|
||||
|
||||
// Extract preferences as JSON
|
||||
let preferences = serde_json::json!({
|
||||
"calendar_selected_date": response.preferences.calendar_selected_date,
|
||||
"calendar_time_increment": response.preferences.calendar_time_increment,
|
||||
"calendar_view_mode": response.preferences.calendar_view_mode,
|
||||
"calendar_theme": response.preferences.calendar_theme,
|
||||
"calendar_colors": response.preferences.calendar_colors,
|
||||
});
|
||||
|
||||
Ok((response.token, response.session_token, credentials.to_string(), preferences))
|
||||
}
|
||||
Err(err) => {
|
||||
web_sys::console::log_1(&format!("❌ Backend error: {}", err).into());
|
||||
|
||||
Reference in New Issue
Block a user