diff --git a/src-tauri/src/assets/auth-cancelled.html b/src-tauri/src/assets/auth-cancelled.html
new file mode 100644
index 0000000..0127441
--- /dev/null
+++ b/src-tauri/src/assets/auth-cancelled.html
@@ -0,0 +1,51 @@
+
+
+
+
+ Friendolls Sign-in Cancelled
+
+
+
+
+
Sign-in cancelled
+
You can close this tab and return to Friendolls to try again.
+
+
+
diff --git a/src-tauri/src/assets/auth-failed.html b/src-tauri/src/assets/auth-failed.html
new file mode 100644
index 0000000..be6c54d
--- /dev/null
+++ b/src-tauri/src/assets/auth-failed.html
@@ -0,0 +1,51 @@
+
+
+
+
+ Friendolls Sign-in Failed
+
+
+
+
+
Sign-in failed
+
Friendolls could not complete the browser handshake. Return to the app and try again.
+
+
+
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index d12b43a..16a65ea 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -27,9 +27,9 @@ use tauri::async_runtime;
use tauri_specta::{collect_commands, collect_events, Builder as SpectaBuilder, ErrorHandlingMode};
use crate::services::app_events::{
- ActiveDollSpriteChanged, AppDataRefreshed, CreateDoll, CursorMoved, EditDoll,
- FriendActiveDollChanged, FriendActiveDollSpritesUpdated, FriendCursorPositionsUpdated,
- FriendDisconnected,
+ ActiveDollSpriteChanged, AppDataRefreshed, AuthFlowUpdated, CreateDoll, CursorMoved,
+ EditDoll, FriendActiveDollChanged, FriendActiveDollSpritesUpdated,
+ FriendCursorPositionsUpdated, FriendDisconnected,
FriendRequestAccepted, FriendRequestDenied, FriendRequestReceived, FriendUserStatusChanged,
InteractionDeliveryFailed, InteractionReceived, SceneInteractiveChanged, SetInteractionOverlay,
Unfriended, UserStatusChanged,
@@ -124,7 +124,8 @@ pub fn run() {
FriendRequestReceived,
FriendRequestAccepted,
FriendRequestDenied,
- Unfriended
+ Unfriended,
+ AuthFlowUpdated
]);
#[cfg(debug_assertions)]
diff --git a/src-tauri/src/services/app_events.rs b/src-tauri/src/services/app_events.rs
index 36910ff..eb18bcd 100644
--- a/src-tauri/src/services/app_events.rs
+++ b/src-tauri/src/services/app_events.rs
@@ -18,6 +18,23 @@ use crate::{
},
};
+#[derive(Debug, Clone, Serialize, Deserialize, Type)]
+#[serde(rename_all = "snake_case")]
+pub enum AuthFlowStatus {
+ Started,
+ Succeeded,
+ Failed,
+ Cancelled,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Type)]
+#[serde(rename_all = "camelCase")]
+pub struct AuthFlowUpdatedPayload {
+ pub provider: String,
+ pub status: AuthFlowStatus,
+ pub message: Option,
+}
+
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
#[tauri_specta(event_name = "cursor-position")]
pub struct CursorMoved(pub CursorPositions);
@@ -93,3 +110,7 @@ pub struct FriendRequestDenied(pub FriendRequestDeniedPayload);
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
#[tauri_specta(event_name = "unfriended")]
pub struct Unfriended(pub UnfriendedPayload);
+
+#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
+#[tauri_specta(event_name = "auth-flow-updated")]
+pub struct AuthFlowUpdated(pub AuthFlowUpdatedPayload);
diff --git a/src-tauri/src/services/auth/api.rs b/src-tauri/src/services/auth/api.rs
index 6a3c182..f2afd93 100644
--- a/src-tauri/src/services/auth/api.rs
+++ b/src-tauri/src/services/auth/api.rs
@@ -8,6 +8,8 @@ use super::storage::{build_auth_pass, save_auth_pass, AuthError, AuthPass};
#[derive(Debug, Deserialize)]
pub struct StartSsoResponse {
pub state: String,
+ #[serde(rename = "authorizeUrl")]
+ pub authorize_url: Option,
}
#[derive(Debug, Deserialize)]
diff --git a/src-tauri/src/services/auth/flow.rs b/src-tauri/src/services/auth/flow.rs
index 7a6da38..d6f2eb3 100644
--- a/src-tauri/src/services/auth/flow.rs
+++ b/src-tauri/src/services/auth/flow.rs
@@ -1,3 +1,4 @@
+use tauri_specta::Event as _;
use tauri_plugin_opener::OpenerExt;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
@@ -5,6 +6,7 @@ use tokio_util::sync::CancellationToken;
use tracing::{error, info, warn};
use crate::get_app_handle;
+use crate::services::app_events::{AuthFlowStatus, AuthFlowUpdated, AuthFlowUpdatedPayload};
use crate::state::clear_auth_flow_state;
use crate::{lock_r, state::FDOLL};
@@ -12,10 +14,17 @@ use super::api::{exchange_sso_code, start_sso};
use super::storage::AuthError;
static AUTH_SUCCESS_HTML: &str = include_str!("../../assets/auth-success.html");
+static AUTH_CANCELLED_HTML: &str = include_str!("../../assets/auth-cancelled.html");
+static AUTH_FAILED_HTML: &str = include_str!("../../assets/auth-failed.html");
pub struct OAuthCallbackParams {
pub state: String,
- pub code: String,
+ pub result: OAuthCallbackResult,
+}
+
+pub enum OAuthCallbackResult {
+ Code(String),
+ Error { message: String, cancelled: bool },
}
pub async fn start_browser_auth_flow(provider: &str) -> Result<(), AuthError> {
@@ -37,48 +46,103 @@ pub async fn start_browser_auth_flow(provider: &str) -> Result<(), AuthError> {
let cancel_token = CancellationToken::new();
{
let mut guard = crate::lock_w!(crate::state::FDOLL);
- guard.auth.oauth_flow.state = Some(start_response.state.clone());
guard.auth.oauth_flow.background_auth_token = Some(cancel_token.clone());
}
let listener = TcpListener::from_std(std_listener)
.map_err(|e| AuthError::ServerBindError(e.to_string()))?;
let expected_state = start_response.state.clone();
- let auth_url = build_authorize_url(provider, &start_response.state)?;
+ let auth_url = match start_response.authorize_url.clone() {
+ Some(authorize_url) => authorize_url,
+ None => build_authorize_url(provider, &start_response.state)?,
+ };
+ let provider_name = provider.to_string();
+
+ if let Err(err) = get_app_handle().opener().open_url(auth_url, None::<&str>) {
+ clear_auth_flow_state();
+ emit_auth_flow_event(
+ provider,
+ AuthFlowStatus::Failed,
+ Some("Friendolls could not open your browser for sign-in.".to_string()),
+ );
+ return Err(err.into());
+ }
+
+ emit_auth_flow_event(provider, AuthFlowStatus::Started, None);
tauri::async_runtime::spawn(async move {
match listen_for_callback(listener, cancel_token.clone()).await {
Ok(params) => {
if params.state != expected_state {
error!("SSO state mismatch");
+ emit_auth_flow_event(
+ &provider_name,
+ AuthFlowStatus::Failed,
+ Some("Sign-in verification failed. Please try again.".to_string()),
+ );
clear_auth_flow_state();
return;
}
- if let Err(err) = exchange_sso_code(¶ms.code).await {
- error!("Failed to exchange SSO code: {}", err);
- clear_auth_flow_state();
- return;
- }
+ match params.result {
+ OAuthCallbackResult::Code(code) => {
+ if let Err(err) = exchange_sso_code(&code).await {
+ error!("Failed to exchange SSO code: {}", err);
+ emit_auth_flow_event(
+ &provider_name,
+ AuthFlowStatus::Failed,
+ Some("Friendolls could not complete sign-in. Please try again.".to_string()),
+ );
+ clear_auth_flow_state();
+ return;
+ }
- clear_auth_flow_state();
- if let Err(err) = super::session::finish_login_session().await {
- error!("Failed to finalize desktop login session: {}", err);
+ clear_auth_flow_state();
+ if let Err(err) = super::session::finish_login_session().await {
+ error!("Failed to finalize desktop login session: {}", err);
+ emit_auth_flow_event(
+ &provider_name,
+ AuthFlowStatus::Failed,
+ Some("Signed in, but Friendolls could not open your session.".to_string()),
+ );
+ } else {
+ emit_auth_flow_event(&provider_name, AuthFlowStatus::Succeeded, None);
+ }
+ }
+ OAuthCallbackResult::Error { message, cancelled } => {
+ clear_auth_flow_state();
+ emit_auth_flow_event(
+ &provider_name,
+ if cancelled {
+ AuthFlowStatus::Cancelled
+ } else {
+ AuthFlowStatus::Failed
+ },
+ Some(message),
+ );
+ }
}
}
Err(AuthError::Cancelled) => {
info!("Auth flow cancelled");
+ emit_auth_flow_event(
+ &provider_name,
+ AuthFlowStatus::Cancelled,
+ Some("Sign-in was cancelled.".to_string()),
+ );
+ clear_auth_flow_state();
}
Err(err) => {
error!("Auth callback listener failed: {}", err);
+ emit_auth_flow_event(
+ &provider_name,
+ AuthFlowStatus::Failed,
+ Some(auth_flow_error_message(&err)),
+ );
clear_auth_flow_state();
}
}
});
- get_app_handle()
- .opener()
- .open_url(auth_url, None::<&str>)?;
-
Ok(())
}
@@ -156,20 +220,36 @@ async fn parse_callback(stream: &mut TcpStream) -> Result