feat: persist session token across app restarts

This commit is contained in:
2026-04-18 01:27:50 -04:00
parent d9590987a5
commit eced53aecd
2 changed files with 75 additions and 10 deletions

View File

@@ -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<User> = 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<String, String> {
@@ -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<User, String> {
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<User> = 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<User, String> {
let mut result: Vec<User> = 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<Vec<User>, String> {
let result: Vec<User> = state
@@ -123,7 +168,7 @@ pub async fn get_contacts(state: State<'_, AppState>) -> Result<Vec<User>, 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<Option<String>, 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())
}

View File

@@ -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,