diff --git a/crates/desktop/artcraft/src/core/commands/app_state/mod.rs b/crates/desktop/artcraft/src/core/commands/app_state/mod.rs new file mode 100644 index 0000000000..33df1ad78c --- /dev/null +++ b/crates/desktop/artcraft/src/core/commands/app_state/mod.rs @@ -0,0 +1 @@ +mod reset_artcraft_command; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/core/commands/app_state/reset_artcraft_command.rs b/crates/desktop/artcraft/src/core/commands/app_state/reset_artcraft_command.rs new file mode 100644 index 0000000000..10eb57bb00 --- /dev/null +++ b/crates/desktop/artcraft/src/core/commands/app_state/reset_artcraft_command.rs @@ -0,0 +1,34 @@ +use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageSuccessResponse; +use crate::core::commands::response::shorthand::ResponseOrErrorMessage; +use crate::core::commands::response::success_response_wrapper::SerializeMarker; +use crate::core::state::app_preferences::app_preferences_manager::AppPreferencesManager; +use crate::core::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory; +use crate::core::state::data_dir::app_data_root::AppDataRoot; +use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager; +use crate::services::storyteller::windows::clear_main_webview_window_storyteller_cookies::clear_main_webview_window_storyteller_cookies; +use anyhow::anyhow; +use errors::AnyhowResult; +use log::{error, info}; +use serde_derive::{Deserialize, Serialize}; +use tauri::{AppHandle, State}; + +#[derive(Serialize)] +pub struct ResetArtcraftCommandResponse { + pub success: bool, +} + +impl SerializeMarker for ResetArtcraftCommandResponse {} + +#[tauri::command] +pub async fn reset_artcraft_command( + app: AppHandle, +) -> ResponseOrErrorMessage { + info!("reset_artcraft_command called"); + + // TODO: Reset local vs. production + + Ok(ResetArtcraftCommandResponse { + success: true, + }.into()) +} + diff --git a/crates/desktop/artcraft/src/core/commands/mod.rs b/crates/desktop/artcraft/src/core/commands/mod.rs index b3264a7ca9..2e81047e07 100644 --- a/crates/desktop/artcraft/src/core/commands/mod.rs +++ b/crates/desktop/artcraft/src/core/commands/mod.rs @@ -1,4 +1,5 @@ pub mod app_preferences; +pub mod app_state; pub mod enqueue; pub mod flip_image; pub mod get_app_info_command; diff --git a/crates/desktop/artcraft/src/core/threads/main_window_thread/main_window_thread.rs b/crates/desktop/artcraft/src/core/threads/main_window_thread/main_window_thread.rs index 4c6d8fe83a..720a8ed896 100644 --- a/crates/desktop/artcraft/src/core/threads/main_window_thread/main_window_thread.rs +++ b/crates/desktop/artcraft/src/core/threads/main_window_thread/main_window_thread.rs @@ -5,14 +5,13 @@ use crate::core::state::window::main_window_size::MainWindowSize; use crate::core::threads::main_window_thread::persist_storyteller_cookies_task::persist_storyteller_cookies_task; use crate::core::threads::main_window_thread::persist_window_position_task::persist_window_position_task; use crate::core::threads::main_window_thread::persist_window_resize_task::persist_window_resize_task; +use crate::core::windows::main_window::constants::MAIN_WINDOW_NAME; use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager; use errors::AnyhowResult; use log::{error, info, warn}; use memory_store::clone_slot::CloneSlot; use tauri::{AppHandle, Manager, Window}; -const MAIN_WINDOW_NAME : &str = "main"; - pub async fn main_window_thread( app: AppHandle, app_data_root: AppDataRoot, diff --git a/crates/desktop/artcraft/src/core/threads/main_window_thread/persist_storyteller_cookies_task.rs b/crates/desktop/artcraft/src/core/threads/main_window_thread/persist_storyteller_cookies_task.rs index 5237aaa7b8..82f69b10ca 100644 --- a/crates/desktop/artcraft/src/core/threads/main_window_thread/persist_storyteller_cookies_task.rs +++ b/crates/desktop/artcraft/src/core/threads/main_window_thread/persist_storyteller_cookies_task.rs @@ -13,8 +13,6 @@ use tauri::{AppHandle, Manager}; use tauri_plugin_http::reqwest_cookie_store::CookieStore; use tauri_plugin_http::Http; -const MAIN_WEBVIEW_NAME: &str = "main"; - const STORYTELLER_ROOT_COOKIE_URL_STR: &str = "https://api.storyteller.ai/"; static STORYTELLER_ROOT_COOKIE_URL: Lazy = Lazy::new(|| { diff --git a/crates/desktop/artcraft/src/core/windows/main_window/constants.rs b/crates/desktop/artcraft/src/core/windows/main_window/constants.rs index cea2c68cb1..62c6e0e84f 100644 --- a/crates/desktop/artcraft/src/core/windows/main_window/constants.rs +++ b/crates/desktop/artcraft/src/core/windows/main_window/constants.rs @@ -3,6 +3,9 @@ /// This is the Artcraft application UX itself. pub const MAIN_WINDOW_NAME : &str = "main"; +/// This is the name of the main window's main webview. +pub const MAIN_WEBVIEW_NAME: &str = "main"; + // TODO: handle pixel density, eg. retina displays /// This is the minimum width to resize the main window in an automated fashion pub const MAIN_WINDOW_MIN_WIDTH : u32 = 800; diff --git a/crates/desktop/artcraft/src/lib.rs b/crates/desktop/artcraft/src/lib.rs index ad2f769808..58d0de8daa 100644 --- a/crates/desktop/artcraft/src/lib.rs +++ b/crates/desktop/artcraft/src/lib.rs @@ -48,6 +48,7 @@ use crate::services::sora::state::sora_task_queue::SoraTaskQueue; use crate::services::sora::threads::sora_task_polling_thread::sora_task_polling_thread; use crate::services::storyteller::commands::storyteller_get_credits_command::storyteller_get_credits_command; use crate::services::storyteller::commands::storyteller_get_subscription_command::storyteller_get_subscription_command; +use crate::services::storyteller::commands::storyteller_purge_credentials_command::storyteller_purge_credentials_command; use crate::services::storyteller::commands::stripe_checkout::storyteller_open_credits_purchase_command::storyteller_open_credits_purchase_command; use crate::services::storyteller::commands::stripe_checkout::storyteller_open_subscription_purchase_command::storyteller_open_subscription_purchase_command; use crate::services::storyteller::commands::stripe_customer_portal::storyteller_open_customer_portal_cancel_plan_command::storyteller_open_customer_portal_cancel_plan_command; @@ -208,6 +209,7 @@ pub fn run() { storyteller_open_customer_portal_switch_plan_command, storyteller_open_customer_portal_update_payment_method_command, storyteller_open_subscription_purchase_command, + storyteller_purge_credentials_command, update_app_preferences_command, ]); diff --git a/crates/desktop/artcraft/src/services/storyteller/commands/mod.rs b/crates/desktop/artcraft/src/services/storyteller/commands/mod.rs index 0053db3198..294ba3f6cd 100644 --- a/crates/desktop/artcraft/src/services/storyteller/commands/mod.rs +++ b/crates/desktop/artcraft/src/services/storyteller/commands/mod.rs @@ -1,3 +1,4 @@ +pub mod storyteller_purge_credentials_command; pub mod storyteller_get_credits_command; pub mod storyteller_get_subscription_command; pub mod stripe_checkout; diff --git a/crates/desktop/artcraft/src/services/storyteller/commands/storyteller_purge_credentials_command.rs b/crates/desktop/artcraft/src/services/storyteller/commands/storyteller_purge_credentials_command.rs new file mode 100644 index 0000000000..65aed23986 --- /dev/null +++ b/crates/desktop/artcraft/src/services/storyteller/commands/storyteller_purge_credentials_command.rs @@ -0,0 +1,56 @@ +use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageSuccessResponse; +use crate::core::commands::response::shorthand::ResponseOrErrorMessage; +use crate::core::commands::response::success_response_wrapper::SerializeMarker; +use crate::core::state::app_preferences::app_preferences_manager::AppPreferencesManager; +use crate::core::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory; +use crate::core::state::data_dir::app_data_root::AppDataRoot; +use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager; +use crate::services::storyteller::windows::clear_main_webview_window_storyteller_cookies::clear_main_webview_window_storyteller_cookies; +use anyhow::anyhow; +use errors::AnyhowResult; +use log::{error, info}; +use serde_derive::{Deserialize, Serialize}; +use tauri::{AppHandle, State}; + +#[derive(Serialize)] +pub struct PurgeStorytellerCredentialsCommandResponse { + pub success: bool, +} + +impl SerializeMarker for PurgeStorytellerCredentialsCommandResponse {} + +#[tauri::command] +pub async fn storyteller_purge_credentials_command( + app: AppHandle, + storyteller_creds_manager: State<'_, StorytellerCredentialManager>, +) -> ResponseOrErrorMessage { + info!("storyteller_purge_credentials_command called"); + + reset(&app, &storyteller_creds_manager) + .await + .map_err(|err| { + error!("Error purging credentials: {:?}", err); + format!("Error purging credentials: {:?}", err) + })?; + + Ok(PurgeStorytellerCredentialsCommandResponse { + success: true, + }.into()) +} + +async fn reset( + app: &AppHandle, + storyteller_creds_manager: &StorytellerCredentialManager +) -> AnyhowResult<()> { + + clear_main_webview_window_storyteller_cookies(&app)?; + + storyteller_creds_manager.clear_credentials()?; + storyteller_creds_manager.delete_persisted_copies_on_disk()?; + + // NB: Twice to be sure. + storyteller_creds_manager.clear_credentials()?; + storyteller_creds_manager.delete_persisted_copies_on_disk()?; + + Ok(()) +} diff --git a/crates/desktop/artcraft/src/services/storyteller/state/storyteller_credential_manager.rs b/crates/desktop/artcraft/src/services/storyteller/state/storyteller_credential_manager.rs index ab7fea29f9..904f1ae784 100644 --- a/crates/desktop/artcraft/src/services/storyteller/state/storyteller_credential_manager.rs +++ b/crates/desktop/artcraft/src/services/storyteller/state/storyteller_credential_manager.rs @@ -78,6 +78,22 @@ impl StorytellerCredentialManager { Ok(()) } + + pub fn delete_persisted_copies_on_disk(&self) -> AnyhowResult<()> { + let creds_dir = self.app_data_root.credentials_dir(); + + let session_file = creds_dir.get_storyteller_session_cookie_file_path(); + if session_file.exists() { + std::fs::remove_file(session_file)?; + } + + let avt_file = creds_dir.get_storyteller_avt_cookie_file_path(); + if avt_file.exists() { + std::fs::remove_file(avt_file)?; + } + + Ok(()) + } } fn persist_session_to_disk(session: &StorytellerSessionCookie, app_data_root: &AppDataRoot) -> AnyhowResult<()> { diff --git a/crates/desktop/artcraft/src/services/storyteller/windows/clear_main_webview_window_storyteller_cookies.rs b/crates/desktop/artcraft/src/services/storyteller/windows/clear_main_webview_window_storyteller_cookies.rs new file mode 100644 index 0000000000..1ab9df358b --- /dev/null +++ b/crates/desktop/artcraft/src/services/storyteller/windows/clear_main_webview_window_storyteller_cookies.rs @@ -0,0 +1,59 @@ +use crate::core::windows::main_window::constants::MAIN_WINDOW_NAME; +use errors::AnyhowResult; +use log::warn; +use tauri::webview::Cookie; +use tauri::{AppHandle, Manager}; + +pub fn clear_main_webview_window_storyteller_cookies(app: &AppHandle) -> AnyhowResult<()> { + let main_webview_window = app.get_webview_window(MAIN_WINDOW_NAME) + .ok_or_else(|| anyhow::anyhow!("Main window not found"))?; + + let cookies = main_webview_window.cookies()?; + + let cookies_to_delete = get_cookies_to_delete(cookies); + + let mut maybe_error = None; + + for cookie in cookies_to_delete { + if let Err(err) = main_webview_window.delete_cookie(cookie) { + warn!("Failed to delete cookie: {:?}", err); + maybe_error = Some(err); + } + } + + if let Some(err) = maybe_error { + return Err(anyhow::anyhow!("Failed to delete some cookies. Here's one failure: {:?}", err)); + } + + Ok(()) +} + +// TODO: This will need to handle local development, too. +fn get_cookies_to_delete(cookies: Vec) -> Vec { + cookies.into_iter() + .filter(|cookie| { + // Define your filtering logic here + // For example, delete cookies with a specific name or domain + match cookie.domain() { + // storyteller.ai + Some("api.storyteller.ai") => true, + Some("storyteller.ai") => true, + Some(domain) if domain.ends_with(".storyteller.ai") => true, + // getartcraft.com + Some("api.getartcraft.com") => true, + Some("getartcraft.com") => true, + Some(domain) if domain.ends_with(".getartcraft.com") => true, + // artcraft.ai + Some("api.artcraft.ai") => true, + Some("artcraft.ai") => true, + Some(domain) if domain.ends_with(".artcraft.ai") => true, + // ignore all others + None => false, + Some(domain) => { + warn!("Ignoring cookie with unrecognized domain: {}", domain); + false + } + } + }) + .collect() +} \ No newline at end of file diff --git a/crates/desktop/artcraft/src/services/storyteller/windows/mod.rs b/crates/desktop/artcraft/src/services/storyteller/windows/mod.rs index 6075910582..ef56e02566 100644 --- a/crates/desktop/artcraft/src/services/storyteller/windows/mod.rs +++ b/crates/desktop/artcraft/src/services/storyteller/windows/mod.rs @@ -1,2 +1,3 @@ +pub mod clear_main_webview_window_storyteller_cookies; pub mod open_storyteller_billing_window; pub mod storyteller_billing_window_thread; \ No newline at end of file diff --git a/frontend/libs/components/settings-modal/src/lib/panes/AccountSettings/ArtcraftAccountBlock.tsx b/frontend/libs/components/settings-modal/src/lib/panes/AccountSettings/ArtcraftAccountBlock.tsx index f5788dc516..26de8bfae8 100644 --- a/frontend/libs/components/settings-modal/src/lib/panes/AccountSettings/ArtcraftAccountBlock.tsx +++ b/frontend/libs/components/settings-modal/src/lib/panes/AccountSettings/ArtcraftAccountBlock.tsx @@ -6,6 +6,7 @@ import { Button } from "@storyteller/ui-button"; import { UsersApi } from "@storyteller/api"; import { UserInfo } from "@storyteller/api"; import { useLoginModalStore } from "@storyteller/ui-login-modal"; +import { invoke } from "@tauri-apps/api/core"; const usersApi = new UsersApi(); @@ -52,7 +53,12 @@ export const ArtcraftAccountBlock = ({ setIsLoggedIn(false); setIsCheckingArtcraftSession(false); globalAccountLogoutCallback(); // TODO: This resets the old global application state + + triggerRecheck(); // Trigger modal to recheck session and potentially open + + await invoke("storyteller_purge_credentials_command"); + } else { triggerRecheck(); // Open login modal instead of redirecting }