From eced53aecd836f1bc4ec3632bbdef141decb8c7c Mon Sep 17 00:00:00 2001 From: Qdust41 Date: Sat, 18 Apr 2026 01:27:50 -0400 Subject: [PATCH] feat: persist session token across app restarts --- src-tauri/src/commands/user.rs | 84 ++++++++++++++++++++++++++++++---- src-tauri/src/lib.rs | 1 + 2 files changed, 75 insertions(+), 10 deletions(-) diff --git a/src-tauri/src/commands/user.rs b/src-tauri/src/commands/user.rs index 3ad57f6..a608f38 100644 --- a/src-tauri/src/commands/user.rs +++ b/src-tauri/src/commands/user.rs @@ -1,14 +1,19 @@ -use tauri::State; +use tauri::{AppHandle, State}; +use tauri_plugin_store::StoreExt; use crate::db::{AppState, SURREAL_ACCESS, SURREAL_DB, SURREAL_NS}; use crate::error::{into_err, AppError}; use crate::models::{Contact, User}; +const SESSION_STORE: &str = "session.json"; +const TOKEN_KEY: &str = "token"; + /// Create a new user account via SurrealDB Record Auth SIGNUP. -/// Returns the created User record. +/// Returns the created User record. Persists the JWT token to disk. #[tauri::command] pub async fn signup( state: State<'_, AppState>, + app_handle: AppHandle, email: String, username: String, password: String, @@ -23,9 +28,10 @@ pub async fn signup( "password": password, }), }; - // into_insecure_token() returns the raw JWT String directly (3.x API). let token = state.db.signup(credentials).await.map_err(into_err)?; - *state.token.lock().unwrap() = Some(token.access.into_insecure_token()); + let token_str = token.access.into_insecure_token(); + *state.token.lock().unwrap() = Some(token_str.clone()); + save_token(&app_handle, &token_str)?; let mut result: Vec = state .db @@ -39,10 +45,11 @@ pub async fn signup( } /// Authenticate an existing user via SurrealDB Record Auth SIGNIN. -/// Returns the JWT token string. +/// Returns the JWT token string. Persists the token to disk. #[tauri::command] pub async fn signin( state: State<'_, AppState>, + app_handle: AppHandle, email: String, password: String, ) -> Result { @@ -57,19 +64,58 @@ pub async fn signin( }; let token_str = state.db.signin(credentials).await.map_err(into_err)?.access.into_insecure_token(); *state.token.lock().unwrap() = Some(token_str.clone()); + save_token(&app_handle, &token_str)?; Ok(token_str) } -/// Clear the current session. Invalidates the token in state. +/// Clear the current session. Invalidates the token in state and removes it from disk. #[tauri::command] -pub async fn signout(state: State<'_, AppState>) -> Result<(), String> { +pub async fn signout( + state: State<'_, AppState>, + app_handle: AppHandle, +) -> Result<(), String> { state.db.invalidate().await.map_err(into_err)?; *state.token.lock().unwrap() = None; + clear_token(&app_handle)?; Ok(()) } +/// Attempt to restore a previous session from the persisted token on disk. +/// Authenticates the DB connection with the stored JWT. +/// Returns the authenticated User on success, or an error if no token exists +/// or the token is expired/invalid (in which case the stored token is also cleared). +#[tauri::command] +pub async fn restore_session( + state: State<'_, AppState>, + app_handle: AppHandle, +) -> Result { + let token_str = load_token(&app_handle)?.ok_or_else(|| { + AppError::Auth("no saved session".into()).to_string() + })?; + + match state.db.authenticate(surrealdb::opt::auth::Token::from(token_str.clone())).await { + Ok(_) => { + *state.token.lock().unwrap() = Some(token_str); + + let mut result: Vec = state + .db + .query("SELECT * FROM $auth") + .await + .map_err(into_err)? + .take(0) + .map_err(into_err)?; + + result.pop().ok_or_else(|| into_err(AppError::Auth("session restored but $auth not set".into()))) + } + Err(_) => { + let _ = clear_token(&app_handle); + *state.token.lock().unwrap() = None; + Err(AppError::Auth("session expired, please sign in again".into()).to_string()) + } + } +} + /// Fetch the currently authenticated user record. -/// Relies on the DB connection being authenticated (token set via signin/signup). #[tauri::command] pub async fn get_me(state: State<'_, AppState>) -> Result { let mut result: Vec = state @@ -109,7 +155,6 @@ pub async fn update_profile( } /// Return the contacts list for the current user. -/// Contacts are `contact` records where `owner = $auth`. #[tauri::command] pub async fn get_contacts(state: State<'_, AppState>) -> Result, String> { let result: Vec = state @@ -123,7 +168,7 @@ pub async fn get_contacts(state: State<'_, AppState>) -> Result, Strin Ok(result) } -/// Add a user to the current user's contact list. Stub — returns the Contact record. +/// Add a user to the current user's contact list. #[tauri::command] pub async fn add_contact( state: State<'_, AppState>, @@ -140,3 +185,22 @@ pub async fn add_contact( result.pop().ok_or_else(|| into_err(AppError::NotFound("contact after create".into()))) } + +// ── Private helpers ─────────────────────────────────────────────────────────── + +fn save_token(app: &AppHandle, token: &str) -> Result<(), String> { + let store = app.store(SESSION_STORE).map_err(|e| e.to_string())?; + store.set(TOKEN_KEY, serde_json::json!(token)); + store.save().map_err(|e| e.to_string()) +} + +fn load_token(app: &AppHandle) -> Result, String> { + let store = app.store(SESSION_STORE).map_err(|e| e.to_string())?; + Ok(store.get(TOKEN_KEY).and_then(|v| v.as_str().map(String::from))) +} + +fn clear_token(app: &AppHandle) -> Result<(), String> { + let store = app.store(SESSION_STORE).map_err(|e| e.to_string())?; + store.delete(TOKEN_KEY); + store.save().map_err(|e| e.to_string()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6aa09c5..24f21a1 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -37,6 +37,7 @@ pub fn run() { commands::user::signin, commands::user::signout, commands::user::get_me, + commands::user::restore_session, commands::user::update_profile, commands::user::get_contacts, commands::user::add_contact,