feat: persist session token across app restarts
This commit is contained in:
@@ -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::db::{AppState, SURREAL_ACCESS, SURREAL_DB, SURREAL_NS};
|
||||||
use crate::error::{into_err, AppError};
|
use crate::error::{into_err, AppError};
|
||||||
use crate::models::{Contact, User};
|
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.
|
/// 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]
|
#[tauri::command]
|
||||||
pub async fn signup(
|
pub async fn signup(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
|
app_handle: AppHandle,
|
||||||
email: String,
|
email: String,
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: String,
|
||||||
@@ -23,9 +28,10 @@ pub async fn signup(
|
|||||||
"password": password,
|
"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)?;
|
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
|
let mut result: Vec<User> = state
|
||||||
.db
|
.db
|
||||||
@@ -39,10 +45,11 @@ pub async fn signup(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Authenticate an existing user via SurrealDB Record Auth SIGNIN.
|
/// 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]
|
#[tauri::command]
|
||||||
pub async fn signin(
|
pub async fn signin(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
|
app_handle: AppHandle,
|
||||||
email: String,
|
email: String,
|
||||||
password: String,
|
password: String,
|
||||||
) -> Result<String, 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();
|
let token_str = state.db.signin(credentials).await.map_err(into_err)?.access.into_insecure_token();
|
||||||
*state.token.lock().unwrap() = Some(token_str.clone());
|
*state.token.lock().unwrap() = Some(token_str.clone());
|
||||||
|
save_token(&app_handle, &token_str)?;
|
||||||
Ok(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]
|
#[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.db.invalidate().await.map_err(into_err)?;
|
||||||
*state.token.lock().unwrap() = None;
|
*state.token.lock().unwrap() = None;
|
||||||
|
clear_token(&app_handle)?;
|
||||||
Ok(())
|
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.
|
/// Fetch the currently authenticated user record.
|
||||||
/// Relies on the DB connection being authenticated (token set via signin/signup).
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_me(state: State<'_, AppState>) -> Result<User, String> {
|
pub async fn get_me(state: State<'_, AppState>) -> Result<User, String> {
|
||||||
let mut result: Vec<User> = state
|
let mut result: Vec<User> = state
|
||||||
@@ -109,7 +155,6 @@ pub async fn update_profile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Return the contacts list for the current user.
|
/// Return the contacts list for the current user.
|
||||||
/// Contacts are `contact` records where `owner = $auth`.
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_contacts(state: State<'_, AppState>) -> Result<Vec<User>, String> {
|
pub async fn get_contacts(state: State<'_, AppState>) -> Result<Vec<User>, String> {
|
||||||
let result: Vec<User> = state
|
let result: Vec<User> = state
|
||||||
@@ -123,7 +168,7 @@ pub async fn get_contacts(state: State<'_, AppState>) -> Result<Vec<User>, Strin
|
|||||||
Ok(result)
|
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]
|
#[tauri::command]
|
||||||
pub async fn add_contact(
|
pub async fn add_contact(
|
||||||
state: State<'_, AppState>,
|
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())))
|
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())
|
||||||
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ pub fn run() {
|
|||||||
commands::user::signin,
|
commands::user::signin,
|
||||||
commands::user::signout,
|
commands::user::signout,
|
||||||
commands::user::get_me,
|
commands::user::get_me,
|
||||||
|
commands::user::restore_session,
|
||||||
commands::user::update_profile,
|
commands::user::update_profile,
|
||||||
commands::user::get_contacts,
|
commands::user::get_contacts,
|
||||||
commands::user::add_contact,
|
commands::user::add_contact,
|
||||||
|
|||||||
Reference in New Issue
Block a user