diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 2c4392a..aa4f91d 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1162,6 +1162,7 @@ dependencies = [ "tauri-plugin-global-shortcut", "tauri-plugin-opener", "tauri-plugin-positioner", + "tauri-plugin-process", "thiserror 1.0.69", "tokio", "tokio-util", @@ -4492,6 +4493,16 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "tauri-plugin-process" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55511a7bf6cd70c8767b02c97bf8134fa434daf3926cfc1be0a0f94132d165a" +dependencies = [ + "tauri", + "tauri-plugin", +] + [[package]] name = "tauri-runtime" version = "2.9.1" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d3f3078..4b15fa5 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -25,6 +25,7 @@ serde_json = "1" tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] } tauri-plugin-global-shortcut = "2" tauri-plugin-positioner = "2" +tauri-plugin-process = "2" reqwest = { version = "0.12.23", features = ["json", "native-tls", "blocking"] } tokio-util = "0.7" ts-rs = "11.0.1" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index ac7e0ad..8eb1d78 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -9,6 +9,7 @@ "dialog:default", "core:event:allow-listen", "core:event:allow-unlisten", - "core:window:allow-close" + "core:window:allow-close", + "process:default" ] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f050718..d2f50a0 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -330,6 +330,20 @@ fn quit_app() -> Result<(), String> { Ok(()) } +#[tauri::command] +fn restart_app() -> Result<(), String> { + let app_handle = get_app_handle(); + app_handle.restart(); + Ok(()) +} + +#[tauri::command] +async fn logout_and_restart() -> Result<(), String> { + crate::services::auth::logout_and_restart() + .await + .map_err(|e| e.to_string()) +} + #[tauri::command] fn start_auth_flow() -> Result<(), String> { // Cancel any in-flight auth listener/state before starting a new one @@ -352,6 +366,7 @@ pub fn run() { .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_positioner::init()) .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_process::init()) .invoke_handler(tauri::generate_handler![ start_cursor_tracking, get_app_data, @@ -373,8 +388,10 @@ pub fn run() { remove_active_doll, recolor_gif_base64, quit_app, + restart_app, open_doll_editor_window, - start_auth_flow + start_auth_flow, + logout_and_restart ]) .setup(|app| { APP_HANDLE diff --git a/src-tauri/src/remotes/mod.rs b/src-tauri/src/remotes/mod.rs index 16eb049..43d77aa 100644 --- a/src-tauri/src/remotes/mod.rs +++ b/src-tauri/src/remotes/mod.rs @@ -1,3 +1,4 @@ pub mod dolls; pub mod friends; pub mod user; +pub mod session; diff --git a/src-tauri/src/remotes/session.rs b/src-tauri/src/remotes/session.rs new file mode 100644 index 0000000..c248281 --- /dev/null +++ b/src-tauri/src/remotes/session.rs @@ -0,0 +1,49 @@ +use reqwest::Error; +use serde_json::json; + +use crate::services::auth::with_auth; +use crate::{lock_r, state::FDOLL}; + +pub struct SessionRemote { + pub base_url: String, + pub client: reqwest::Client, +} + +impl SessionRemote { + pub fn new() -> Self { + let guard = lock_r!(FDOLL); + Self { + base_url: guard + .app_config + .api_base_url + .as_ref() + .expect("App configuration error") + .clone(), + client: guard + .clients + .as_ref() + .expect("App configuration error") + .http_client + .clone(), + } + } + + pub async fn logout( + &self, + refresh_token: &str, + session_state: Option<&str>, + ) -> Result<(), Error> { + let url = format!("{}/users/logout", self.base_url); + let body = json!({ + "refreshToken": refresh_token, + "sessionState": session_state, + }); + let resp = with_auth(self.client.post(url)) + .await + .json(&body) + .send() + .await?; + resp.error_for_status()?; + Ok(()) + } +} diff --git a/src-tauri/src/services/auth.rs b/src-tauri/src/services/auth.rs index 8462258..7d81f96 100644 --- a/src-tauri/src/services/auth.rs +++ b/src-tauri/src/services/auth.rs @@ -388,6 +388,47 @@ pub fn logout() -> Result<(), OAuthError> { Ok(()) } +/// Convenience helper to perform logout side effects before app restart. +pub async fn logout_and_restart() -> Result<(), OAuthError> { + // capture tokens and base_url before clearing for backend revocation + let (refresh_token, session_state, base_url) = { + let guard = lock_r!(FDOLL); + ( + guard.auth_pass.as_ref().map(|p| p.refresh_token.clone()), + guard + .auth_pass + .as_ref() + .map(|p| p.session_state.clone()), + guard + .app_config + .api_base_url + .as_ref() + .cloned() + .unwrap_or_default(), + ) + }; + + logout()?; + + if !base_url.is_empty() { + if let Some(refresh_token) = refresh_token.as_deref() { + let session_remote = crate::remotes::session::SessionRemote::new(); + if let Err(err) = session_remote + .logout(refresh_token, session_state.as_deref()) + .await + { + warn!("Failed to revoke session on server: {}", err); + } + } else { + warn!("No refresh token available to revoke on server"); + } + } + + let app_handle = get_app_handle(); + app_handle.restart(); + Ok(()) +} + /// Helper to add authentication header to a request builder if tokens are available. /// /// # Example diff --git a/src/routes/app-menu/tabs/preferences.svelte b/src/routes/app-menu/tabs/preferences.svelte index d68d6ef..bbef75e 100644 --- a/src/routes/app-menu/tabs/preferences.svelte +++ b/src/routes/app-menu/tabs/preferences.svelte @@ -2,11 +2,27 @@ import { invoke } from "@tauri-apps/api/core"; import { appData } from "../../../events/app-data"; import Power from "../../../assets/icons/power.svelte"; + + let signingOut = false; + + async function handleSignOut() { + if (signingOut) return; + signingOut = true; + try { + await invoke("logout_and_restart"); + } catch (error) { + console.error("Failed to sign out", error); + signingOut = false; + } + }
-
+

{$appData?.user?.name}'s preferences

+