This commit is contained in:
2025-12-31 21:24:37 +08:00
parent ed3e0a21ae
commit 401923ef4c
8 changed files with 140 additions and 3 deletions

11
src-tauri/Cargo.lock generated
View File

@@ -1162,6 +1162,7 @@ dependencies = [
"tauri-plugin-global-shortcut", "tauri-plugin-global-shortcut",
"tauri-plugin-opener", "tauri-plugin-opener",
"tauri-plugin-positioner", "tauri-plugin-positioner",
"tauri-plugin-process",
"thiserror 1.0.69", "thiserror 1.0.69",
"tokio", "tokio",
"tokio-util", "tokio-util",
@@ -4492,6 +4493,16 @@ dependencies = [
"thiserror 2.0.17", "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]] [[package]]
name = "tauri-runtime" name = "tauri-runtime"
version = "2.9.1" version = "2.9.1"

View File

@@ -25,6 +25,7 @@ serde_json = "1"
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }
tauri-plugin-global-shortcut = "2" tauri-plugin-global-shortcut = "2"
tauri-plugin-positioner = "2" tauri-plugin-positioner = "2"
tauri-plugin-process = "2"
reqwest = { version = "0.12.23", features = ["json", "native-tls", "blocking"] } reqwest = { version = "0.12.23", features = ["json", "native-tls", "blocking"] }
tokio-util = "0.7" tokio-util = "0.7"
ts-rs = "11.0.1" ts-rs = "11.0.1"

View File

@@ -9,6 +9,7 @@
"dialog:default", "dialog:default",
"core:event:allow-listen", "core:event:allow-listen",
"core:event:allow-unlisten", "core:event:allow-unlisten",
"core:window:allow-close" "core:window:allow-close",
"process:default"
] ]
} }

View File

@@ -330,6 +330,20 @@ fn quit_app() -> Result<(), String> {
Ok(()) 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] #[tauri::command]
fn start_auth_flow() -> Result<(), String> { fn start_auth_flow() -> Result<(), String> {
// Cancel any in-flight auth listener/state before starting a new one // 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_opener::init())
.plugin(tauri_plugin_positioner::init()) .plugin(tauri_plugin_positioner::init())
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_process::init())
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
start_cursor_tracking, start_cursor_tracking,
get_app_data, get_app_data,
@@ -373,8 +388,10 @@ pub fn run() {
remove_active_doll, remove_active_doll,
recolor_gif_base64, recolor_gif_base64,
quit_app, quit_app,
restart_app,
open_doll_editor_window, open_doll_editor_window,
start_auth_flow start_auth_flow,
logout_and_restart
]) ])
.setup(|app| { .setup(|app| {
APP_HANDLE APP_HANDLE

View File

@@ -1,3 +1,4 @@
pub mod dolls; pub mod dolls;
pub mod friends; pub mod friends;
pub mod user; pub mod user;
pub mod session;

View File

@@ -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(())
}
}

View File

@@ -388,6 +388,47 @@ pub fn logout() -> Result<(), OAuthError> {
Ok(()) 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. /// Helper to add authentication header to a request builder if tokens are available.
/// ///
/// # Example /// # Example

View File

@@ -2,11 +2,27 @@
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { appData } from "../../../events/app-data"; import { appData } from "../../../events/app-data";
import Power from "../../../assets/icons/power.svelte"; 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;
}
}
</script> </script>
<div class="size-full flex flex-col justify-between"> <div class="size-full flex flex-col justify-between">
<div> <div class="flex flex-col gap-2">
<p>{$appData?.user?.name}'s preferences</p> <p>{$appData?.user?.name}'s preferences</p>
<button class="btn" class:btn-disabled={signingOut} onclick={handleSignOut}>
{signingOut ? "Signing out..." : "Sign out"}
</button>
</div> </div>
<div class="w-full flex flex-row justify-between"> <div class="w-full flex flex-row justify-between">
<div></div> <div></div>