login window + UI

This commit is contained in:
Brandon Thomas committed 2025-08-15 01:35:15 -04:00
1 parent 94873f7d17
commit 674fc0d01e
15 files changed
+394 -11

No files matched your search

@@ -169,6 +169,8 @@ mod tests {
use errors::AnyhowResult;
use filesys::read_to_trimmed_string::read_to_trimmed_string;
// Get channel id via:
// https://identitytoolkit.googleapis.com/v1/accounts:lookup?key=AIzaSyAjizp68NsH3JGUS0EyLXsChW4fN0A92tM
#[ignore]
#[tokio::test]
async fn test() -> AnyhowResult<()> {
+3 -1
View File
@@ -7,8 +7,8 @@ use crate::core::commands::app_preferences::get_app_preferences_command::get_app
use crate::core::commands::app_preferences::update_app_preference_command::update_app_preferences_command;
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::enqueue_text_to_image_command;
use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::enqueue_image_bg_removal_command;
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::enqueue_image_inpaint_command;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::enqueue_contextual_edit_image_command;
use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::enqueue_image_inpaint_command;
use crate::core::commands::enqueue::object::enqueue_image_to_3d_object_command::enqueue_image_to_3d_object_command;
use crate::core::commands::enqueue::video::enqueue_image_to_video_command::enqueue_image_to_video_command;
use crate::core::commands::flip_image::flip_image;
@@ -32,6 +32,7 @@ use crate::services::fal::commands::set_fal_api_key_command::set_fal_api_key_com
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
use crate::services::fal::threads::fal_task_polling_thread::fal_task_polling_thread;
use crate::services::midjourney::commands::open_midjourney_login_command::open_midjourney_login_command;
use crate::services::sora::commands::check_sora_session_command::check_sora_session_command;
use crate::services::sora::commands::open_sora_login_command::open_sora_login_command;
use crate::services::sora::commands::sora_image_generation_command::sora_image_generation_command;
@@ -159,6 +160,7 @@ pub fn run() {
get_provider_order_command,
load_without_cors_command,
open_sora_login_command,
open_midjourney_login_command,
platform_info_command,
set_fal_api_key_command,
set_provider_order_command,
@@ -0,0 +1,2 @@
pub mod open_midjourney_login_command;
pub mod clear_midjourney_credentials_command;
@@ -0,0 +1,38 @@
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::services::midjourney::windows::open_midjourney_login_window::open_midjourney_login_window;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::windows::sora_login_window::open_sora_login_window::open_sora_login_window;
use errors::AnyhowResult;
use log::{error, info};
use tauri::{AppHandle, State};
#[tauri::command]
pub async fn open_midjourney_login_command(
app: AppHandle,
app_data_root: State<'_, AppDataRoot>,
sora_creds_manager: State<'_, SoraCredentialManager>,
) -> Result<String, String> {
info!("open_midjourney_command called");
do_open_login(&app, &app_data_root, &sora_creds_manager)
.await
.map_err(|err| {
error!("Error opening login: {:?}", err);
format!("Error opening login: {:?}", err)
})?;
Ok("result".to_string())
}
async fn do_open_login(
app: &AppHandle,
app_data_root: &AppDataRoot,
sora_creds_manager: &SoraCredentialManager,
) -> AnyhowResult<()> {
info!("Building login window...");
open_midjourney_login_window(app, app_data_root, sora_creds_manager).await?;
info!("Done.");
Ok(())
}
@@ -0,0 +1,4 @@
pub mod threads;
pub mod state;
pub mod commands;
pub mod windows;
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,148 @@
use crate::core::events::sendable_event_trait::SendableEvent;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::services::midjourney::windows::open_midjourney_login_window::LOGIN_WINDOW_NAME;
use crate::services::sora::events::sora_login_success_event::SoraLoginSuccessEvent;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::windows::sora_login_window::extract_sora_webview_cookies::extract_sora_webview_cookies;
use anyhow::anyhow;
use errors::AnyhowResult;
use log::{error, info};
use openai_sora_client::creds::sora_credential_set::SoraCredentialSet;
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use openai_sora_client::utils::has_session_cookie::{has_session_cookie, SessionCookiePresence};
use tauri::{AppHandle, Manager, WebviewWindow};
pub async fn midjourney_login_thread(
app: AppHandle,
app_data_root: AppDataRoot,
sora_creds_manager: SoraCredentialManager
) {
loop {
let login_webview_window = match app.get_webview_window(LOGIN_WINDOW_NAME) {
Some(webview) => webview,
None => {
info!("Exit midjourney login thread.");
return; // NB: Only exit if we don't have the webview.
}
};
let result = check_login_window(
&app,
&login_webview_window,
&app_data_root,
&sora_creds_manager,
).await;
match result {
Err(err) => {
error!("Error checking login window: {:?}", err);
}
Ok(false) => {} // Continue iteration and try again...
Ok(true) => {
info!("Successfully saved cookies from login window. Closing.");
if let Err(err) = login_webview_window.close() {
error!("Error closing login window: {:?}", err);
}
return;
}
}
tokio::time::sleep(std::time::Duration::from_millis(2_000)).await;
}
}
/// Returns true if we can exit.
async fn check_login_window(
app_handle: &AppHandle,
webview_window: &WebviewWindow,
app_data_root: &AppDataRoot,
sora_credential_manager: &SoraCredentialManager,
) -> AnyhowResult<bool> {
/* Login flow looks like this:
1. Start: https://sora.chatgpt.com/
2. Login Start: https://chatgpt.com/auth/login?next=%2Fsora%2F [this is where we start]
3. Login Continue: https://auth.openai.com/log-in
4. SSO / Google Login (etc): https://accounts.google.com/v3/signin/challenge/pwd?[query]
5. Done / Landing: https://sora.chatgpt.com/explore
*/
/*
- Do not use credential manager until the end (we don't load old cookies!)
- Check if we're on the correct domain, if not exit? (or inverse, that we're not in login flow)
*/
let hostname = get_hostname(webview_window)?;
let mut maybe_at_destination = false;
match hostname.as_str() {
"sora.com" | // Old destination
"sora.chatgpt.com" // New destination
=> {
maybe_at_destination = true;
}
// chatgpt.com/auth is also an auth domain
"auth.openai.com" |
"accounts.google.com" |
"accounts.youtube.com" |
"login.live.com" |
"appleid.apple.com"
=> {
// NB: We're in auth flow.
info!("Sora login webview is in auth flow; hostname `{}`.", hostname);
return Ok(false)
}
_ => {}, // We just don't know...
}
let webview_cookies = extract_sora_webview_cookies(webview_window)?.trim().to_string();
let session_cookie_presence = has_session_cookie(&webview_cookies)
.unwrap_or_else(|err| {
error!("Failed to check for session cookie: {:?}", err);
SessionCookiePresence::MaybePresent
});
info!("Sora login webview is at hostname `{}`; cookie status: {:?}.", hostname, session_cookie_presence);
match session_cookie_presence {
SessionCookiePresence::Absent => {
info!("Session cookies are absent.");
return Ok(false);
}
_ => {},
}
let mut new_credentials =
SoraCredentialSet::initialize_with_just_cookies_str(&webview_cookies);
let _upgraded = maybe_upgrade_or_renew_session(&mut new_credentials).await?;
// TODO(bt): Race conditions ahead.
sora_credential_manager.clear_credentials()?;
sora_credential_manager.try_purge_credentials_from_disk();
sora_credential_manager.set_credentials(&new_credentials)?;
sora_credential_manager.persist_all_to_disk()?;
// NB: Event sent to the frontend for the login flow. We shouldn't rely on just this
// alone as it could be brittle if the events aren't caught.
let event = SoraLoginSuccessEvent {};
if let Err(err) = event.send(app_handle) {
error!("Error sending Sora login success event: {:?}", err);
}
Ok(true)
}
fn get_hostname(webview: &WebviewWindow) -> AnyhowResult<String> {
let url = webview.url()?;
let url_hostname= url.host()
.ok_or(anyhow!("no host in url"))?
.to_string();
Ok(url_hostname)
}
@@ -0,0 +1,2 @@
pub mod open_midjourney_login_window;
pub mod midjourney_login_window_thread;
@@ -0,0 +1,78 @@
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::utils::clear_all_webview_cookies::clear_all_webview_cookies;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::windows::sora_login_window::sora_login_thread::sora_login_thread;
use anyhow::anyhow;
use errors::AnyhowResult;
use once_cell::sync::Lazy;
use reqwest::Url;
use std::time::Duration;
use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder};
/// Name of the window
pub (super) const LOGIN_WINDOW_NAME: &str = "midjourney_login_window";
pub (super) const START_URL_STR: &str = "https://google.com";
pub (super) static START_URL: Lazy<Url> = Lazy::new(|| {
Url::parse(START_URL_STR).expect("URL should parse")
});
pub (super) const MIDJOURNEY_HOMEPAGE_URL_STR: &str = "https://www.midjourney.com/";
pub (super) static MIDJOURNEY_HOMEPAGE_URL: Lazy<Url> = Lazy::new(|| {
Url::parse(MIDJOURNEY_HOMEPAGE_URL_STR).expect("URL should parse")
});
pub (super) const MIDJOURNEY_LOGIN_URL_STR: &str = "https://www.midjourney.com/auth/signin";
pub (super) static MIDJOURNEY_LOGIN_URL: Lazy<Url> = Lazy::new(|| {
Url::parse(MIDJOURNEY_LOGIN_URL_STR).expect("URL should parse")
});
pub async fn open_midjourney_login_window(
app: &AppHandle,
app_data_root: &AppDataRoot,
sora_creds_manager: &SoraCredentialManager,
) -> AnyhowResult<()> {
if app.get_window(LOGIN_WINDOW_NAME).is_some() {
return Err(anyhow!("Login window already open"));
}
let url = WebviewUrl::External(START_URL.clone());
let window = WebviewWindowBuilder::new(app, LOGIN_WINDOW_NAME, url)
//.user_agent(openai_sora_client::credentials::USER_AGENT)
.always_on_top(false)
.title("Login to Midjourney")
.center()
.resizable(true)
.visible(true)
.closable(true)
.min_inner_size(200.0, 800.0)
.focused(true)
.devtools(true)
.build()?;
let webview = window.get_webview(LOGIN_WINDOW_NAME)
.ok_or_else(|| anyhow!("no webview found"))?;
clear_all_webview_cookies(&webview)?;
webview.navigate(MIDJOURNEY_HOMEPAGE_URL.clone())?;
// NB: We're starting to get Cloudflare protection screens. Let's try to avoid.
tokio::time::sleep(Duration::from_millis(100)).await;
webview.navigate(MIDJOURNEY_LOGIN_URL.clone())?;
let app_handle = app.clone();
let app_data_root = app_data_root.clone();
let sora_creds_manager = sora_creds_manager.clone();
let _ = tauri::async_runtime::spawn(async move {
sora_login_thread(app_handle, app_data_root, sora_creds_manager).await;
});
Ok(())
}
@@ -1,3 +1,4 @@
pub mod fal;
pub mod midjourney;
pub mod sora;
pub mod storyteller;
@@ -10,23 +10,23 @@ use std::time::Duration;
use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder};
/// Name of the window
pub const LOGIN_WINDOW_NAME: &str = "login_window";
pub (super) const LOGIN_WINDOW_NAME: &str = "login_window";
pub const START_URL_STR: &str = "https://google.com";
pub (super) const START_URL_STR: &str = "https://google.com";
pub static START_URL: Lazy<Url> = Lazy::new(|| {
pub (super) static START_URL: Lazy<Url> = Lazy::new(|| {
Url::parse(START_URL_STR).expect("URL should parse")
});
pub const SORA_HOMEPAGE_URL_STR: &str = "https://sora.com/";
pub (super) const SORA_HOMEPAGE_URL_STR: &str = "https://sora.com/";
pub static SORA_HOMEPAGE_URL : Lazy<Url> = Lazy::new(|| {
pub (super) static SORA_HOMEPAGE_URL : Lazy<Url> = Lazy::new(|| {
Url::parse(SORA_HOMEPAGE_URL_STR).expect("URL should parse")
});
pub const SORA_LOGIN_URL_STR: &str = "https://chatgpt.com/auth/login?next=%2Fsora%2F";
pub (super) const SORA_LOGIN_URL_STR: &str = "https://chatgpt.com/auth/login?next=%2Fsora%2F";
pub static SORA_LOGIN_URL: Lazy<Url> = Lazy::new(|| {
pub (super) static SORA_LOGIN_URL: Lazy<Url> = Lazy::new(|| {
Url::parse(SORA_LOGIN_URL_STR).expect("URL should parse")
});
@@ -1,11 +1,9 @@
import { useState } from "react";
import { Button } from "@storyteller/ui-button";
import { Input } from "@storyteller/ui-input";
import { SoraAccountButton } from "./SoraAccountButton";
import { SoraAccountBlock } from "./SoraAccountBlock";
import { ArtcraftAccountBlock } from "./ArtcraftAccountBlock";
import { FalApiKeyBlock } from "./FalApiKeyBlock";
import { OpenAIApiKeyBlock } from "./OpenAIKeyBlock";
import { MidjourneyAccountBlock } from "./MidjourneyAccountBlock";
interface AccountInfo {
username: string;
@@ -58,6 +56,7 @@ export const AccountSettingsPane = ({
*/}
<ArtcraftAccountBlock globalAccountLogoutCallback={globalAccountLogoutCallback}/>
<MidjourneyAccountBlock />
<SoraAccountBlock />
<FalApiKeyBlock />
<OpenAIApiKeyBlock/>
@@ -0,0 +1,107 @@
import { Button } from "@storyteller/ui-button";
import { invoke } from "@tauri-apps/api/core";
export const MidjourneyAccountBlock = () => {
/*
const [midjourneySession, setMidjourneySession] = useState<CheckSoraSessionResult| undefined>(undefined);
const [isCheckingMidjourneySession, setIsCheckingMidjourneySession] = useState(false);
useSoraLoginListener(() => {
setIsCheckingMidjourneySession(true);
CheckSoraSession()
.then((result) => {
setMidjourneySession(result);
localStorage.setItem(
cacheKey,
JSON.stringify({ session: result, timestamp: Date.now() })
);
})
.finally(() => setIsCheckingMidjourneySession(false));
});
useEffect(() => {
const fetchSession = async () => {
setIsCheckingMidjourneySession(true);
try {
const cached = localStorage.getItem(cacheKey);
if (cached) {
const { session, timestamp } = JSON.parse(cached);
if (Date.now() - timestamp < cacheTtlMs) {
setMidjourneySession(session);
setIsCheckingMidjourneySession(false);
return;
}
}
const result = await CheckSoraSession();
setMidjourneySession(result);
localStorage.setItem(
cacheKey,
JSON.stringify({ session: result, timestamp: Date.now() })
);
} catch (e) {
console.error("Error fetching Sora session", e);
setMidjourneySession(undefined);
} finally {
setIsCheckingMidjourneySession(false);
}
};
fetchSession();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [cacheKey, cacheTtlMs]);
*/
const handleMidjourneyButton = async () => {
//if (isCheckingMidjourneySession) return;
//if (midjourneySession?.state === SoraSessionState.Valid) {
// setIsCheckingMidjourneySession(true);
// await LogoutSoraSession();
// setMidjourneySession({state: SoraSessionState.NotSetUp});
// localStorage.removeItem(cacheKey);
// setIsCheckingMidjourneySession(false);
//} else {
// setIsCheckingMidjourneySession(true);
// try {
// await invoke("open_sora_login_command");
// } catch (e) {
// } finally {
// setIsCheckingMidjourneySession(false);
// }
//}
try {
await invoke("open_midjourney_login_command");
} catch (e) {
} finally {
}
};
return(
<div className="flex justify-between items-center">
<span>Midjourney Account:</span>
<pre>{"email@domain.com (later)"}</pre>
<Button
//variant={
// midjourneySession?.state === SoraSessionState.Valid && !isCheckingMidjourneySession
// ? "destructive"
// : midjourneySession?.state === SoraSessionState.NotSetUp
// ? "primary"
// : "secondary"
//}
className="h-[30px]"
onClick={handleMidjourneyButton}
//disabled={isCheckingMidjourneySession}
>
{/*{isCheckingMidjourneySession ? (
<FontAwesomeIcon
icon={faSpinnerThird}
className="animate-spin text-sm"
/>
) : midjourneySession?.state === SoraSessionState.Valid ? (
"Disconnect"
) : (
"Connect"
)}*/}
Connect
</Button>
</div>
)
}