From 115daafcf7bec29936e29db20bb428afcc005d39 Mon Sep 17 00:00:00 2001
From: Brandon Thomas
Date: Sat, 26 Sep 2026 00:41:23 -0700
Subject: [PATCH] login bridge
---
.../src/users/session_info.rs | 11 ++
.../src/endpoints/users/login_challenges.rs | 9 +-
.../src/endpoints/users/mod.rs | 1 +
.../src/endpoints/users/password_auth.rs | 38 +++++
crates/desktop/artcraft/src/lib.rs | 5 +-
.../storyteller_login_bridge_commands.rs | 141 ++++++++++++------
.../storyteller_login_bridge_tests.rs | 51 ++++++-
.../src/lib/DesktopLoginBridge.tsx | 3 +-
.../login-modal/src/lib/NativeLoginBridge.ts | 8 +
.../login-modal/src/lib/login-modal.spec.tsx | 95 +++++++++++-
.../login-modal/src/lib/login-modal.tsx | 98 ++++--------
11 files changed, 337 insertions(+), 123 deletions(-)
create mode 100644 crates/api_clients/artcraft/artcraft_client/src/endpoints/users/password_auth.rs
diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/users/session_info.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/users/session_info.rs
index 09b6d26a40..190a917dea 100644
--- a/crates/api_clients/artcraft/artcraft_api_defs/src/users/session_info.rs
+++ b/crates/api_clients/artcraft/artcraft_api_defs/src/users/session_info.rs
@@ -1,6 +1,7 @@
use crate::common::responses::user_details_light::UserDetailsLight;
use enums::by_table::users::user_feature_flag::UserFeatureFlag;
use serde_derive::{Deserialize, Serialize};
+use serde::{Deserialize as DeserializeTrait, Deserializer};
use std::collections::BTreeSet;
use tokens::tokens::users::UserToken;
use utoipa::ToSchema;
@@ -41,6 +42,7 @@ pub struct SessionUserInfo {
/// Collection of feature / rollout flags
/// This is the proper place to detect if a user has access to some rollout (non-paywall) feature.
/// NB: The BTreeSet maintains order so React doesn't introduce re-render state bugs when order changes
+ #[serde(default, deserialize_with = "deserialize_known_feature_flags")]
pub maybe_feature_flags: BTreeSet,
// Premium plans:
@@ -97,3 +99,12 @@ pub struct SessionInfoSuccessResponse {
pub logged_in: bool,
pub user: Option,
}
+
+// Server rollouts may introduce flags before a desktop release understands them.
+// Ignore only unknown flag names; malformed values and authentication fields
+// still fail decoding. Unknown flags never grant a client-side capability.
+fn deserialize_known_feature_flags<'de, D>(deserializer: D) -> Result, D::Error>
+where D: Deserializer<'de> {
+ let names = Vec::::deserialize(deserializer)?;
+ Ok(names.into_iter().filter_map(|name| UserFeatureFlag::from_str(&name).ok()).collect())
+}
diff --git a/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/login_challenges.rs b/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/login_challenges.rs
index 2116c826cf..dbdf063f6e 100644
--- a/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/login_challenges.rs
+++ b/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/login_challenges.rs
@@ -69,9 +69,10 @@ impl LoginChallengeClient {
return false;
};
let origin_allowed = match self.api_host {
- ApiHost::Localhost { .. } => ["http://localhost:4201", "http://127.0.0.1:4201"]
+ ApiHost::Localhost { .. } => ["http://localhost:4200", "http://127.0.0.1:4200", "http://localhost:4201", "http://127.0.0.1:4201"]
+ .contains(&url.origin().ascii_serialization().as_str()),
+ ApiHost::Storyteller => ["https://app.getartcraft.com", "https://getartcraft.com", "https://www.getartcraft.com"]
.contains(&url.origin().ascii_serialization().as_str()),
- ApiHost::Storyteller => url.origin().ascii_serialization() == "https://app.getartcraft.com",
ApiHost::FakeYou => false,
};
origin_allowed
@@ -126,14 +127,14 @@ impl LoginChallengeClient {
Ok(result)
}
- fn request(&self, method: Method, path: &str) -> RequestBuilder {
+ pub(crate) fn request(&self, method: Method, path: &str) -> RequestBuilder {
self
.client
.request(method, format!("{}{path}", self.api_origin()))
.header("Accept", "application/json")
}
- async fn send(
+ pub(crate) async fn send(
&self,
request: RequestBuilder,
path: &str,
diff --git a/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/mod.rs b/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/mod.rs
index e3e166c558..e391a7f44a 100644
--- a/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/mod.rs
+++ b/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/mod.rs
@@ -1 +1,2 @@
pub mod login_challenges;
+pub mod password_auth;
diff --git a/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/password_auth.rs b/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/password_auth.rs
new file mode 100644
index 0000000000..6804164b9e
--- /dev/null
+++ b/crates/api_clients/artcraft/artcraft_client/src/endpoints/users/password_auth.rs
@@ -0,0 +1,38 @@
+use reqwest::Method;
+use serde::{Deserialize, Serialize};
+
+use super::login_challenges::{LoginChallengeClient, LoginChallengeClientError};
+
+// No Debug: these request/response types contain credentials.
+#[derive(Deserialize, Serialize)]
+pub struct PasswordLoginRequest {
+ pub username_or_email: String,
+ pub password: String,
+}
+
+#[derive(Deserialize, Serialize)]
+pub struct PasswordSignupRequest {
+ pub username: String,
+ pub email_address: String,
+ pub password: String,
+ pub password_confirmation: String,
+ pub signup_source: String,
+}
+
+#[derive(Deserialize)]
+pub struct PasswordAuthResponse {
+ pub success: bool,
+ pub signed_session: Option,
+}
+
+impl LoginChallengeClient {
+ pub async fn password_login(&self, request: &PasswordLoginRequest) -> Result<(PasswordAuthResponse, Option), LoginChallengeClientError> {
+ let path = "/v1/login";
+ self.send(self.request(Method::POST, path).json(request), path).await
+ }
+
+ pub async fn password_signup(&self, request: &PasswordSignupRequest) -> Result<(PasswordAuthResponse, Option), LoginChallengeClientError> {
+ let path = "/v1/create_account";
+ self.send(self.request(Method::POST, path).json(request), path).await
+ }
+}
diff --git a/crates/desktop/artcraft/src/lib.rs b/crates/desktop/artcraft/src/lib.rs
index 3590a7e985..389db3cb54 100644
--- a/crates/desktop/artcraft/src/lib.rs
+++ b/crates/desktop/artcraft/src/lib.rs
@@ -63,7 +63,7 @@ use crate::services::sora::commands::sora_logout_command::sora_logout_command;
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::sora::threads::sora_task_polling::sora_task_polling_thread::sora_task_polling_thread;
-use crate::services::storyteller::commands::storyteller_login_bridge_commands::{DesktopLoginBridgeState, storyteller_create_login_challenge_command, storyteller_poll_login_challenge_command, storyteller_cancel_login_challenge_command};
+use crate::services::storyteller::commands::storyteller_login_bridge_commands::{DesktopLoginBridgeState, storyteller_get_login_session_command, storyteller_password_login_command, storyteller_password_signup_command, storyteller_create_login_challenge_command, storyteller_poll_login_challenge_command, storyteller_cancel_login_challenge_command};
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;
@@ -261,6 +261,9 @@ pub fn run() {
set_provider_order_command,
sora_get_credential_info_command,
sora_logout_command,
+ storyteller_get_login_session_command,
+ storyteller_password_login_command,
+ storyteller_password_signup_command,
storyteller_create_login_challenge_command,
storyteller_poll_login_challenge_command,
storyteller_cancel_login_challenge_command,
diff --git a/crates/desktop/artcraft/src/services/storyteller/commands/storyteller_login_bridge_commands.rs b/crates/desktop/artcraft/src/services/storyteller/commands/storyteller_login_bridge_commands.rs
index bbd4538588..23469d5457 100644
--- a/crates/desktop/artcraft/src/services/storyteller/commands/storyteller_login_bridge_commands.rs
+++ b/crates/desktop/artcraft/src/services/storyteller/commands/storyteller_login_bridge_commands.rs
@@ -7,6 +7,7 @@ use artcraft_client::endpoints::users::login_challenges::{
LoginChallengeClient, LoginChallengeClientError,
};
use artcraft_client::utils::api_host::ApiHost;
+use artcraft_client::endpoints::users::password_auth::{PasswordLoginRequest, PasswordSignupRequest};
use chrono::{DateTime, Utc};
use log::{info, warn};
use serde::Serialize;
@@ -90,6 +91,58 @@ pub async fn storyteller_cancel_login_challenge_command(
Ok(())
}
+#[tauri::command]
+pub async fn storyteller_get_login_session_command(app: AppHandle, config: State<'_, AppEnvConfigs>) -> Result
- {
+ {!isSignUp && { authGeneration.current += 1; }} onSuccess={(user) => {
+ authGeneration.current += 1;
onArtCraftAuthSuccess?.(user);
handleClose();
- }} />
+ }} />}