first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
@@ -0,0 +1,82 @@
use galaxy_graphql::scalars::time::ServerTimestamp;
use galaxyui::AppContext;
use galaxyui_extras::secure_storage;
use serde::{Deserialize, Serialize};
use galaxyui_core::AppContext;
use galaxyui_extras::secure_storage::{self, AppContextExt};
use super::{AnonymousUserType, FirebaseAuthTokens, PersonalObjectLimits, UserMetadata};
use crate::UserUid;
const USER_STORAGE_KEY: &str = "User";
/// Helper function to set `true` as the default for a serde field on PersistedUser.
fn default_as_true() -> bool {
true
}
/// The persisted representation of a user, serialized to/from the user's keychain.
/// This struct must remain backwards compatible with the existing keychain JSON format.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PersistedUser {
/// Information about the user's authentication through Firebase.
#[serde(rename = "id_token")]
pub auth_tokens: FirebaseAuthTokens,
/// DO NOT USE! This used to be one of the two places we stored the user's Firebase refresh
/// token. Now, all callers should go through `auth_tokens` to access it.
#[serde(default)]
#[deprecated = "use auth_tokens.refresh_token instead"]
pub refresh_token: String,
/// The Firebase UID of this user.
pub local_id: UserUid,
/// Metadata about the user.
#[serde(flatten)]
pub metadata: UserMetadata,
/// Whether or not the user is onboarded.
#[serde(default = "default_as_true")]
pub is_onboarded: bool,
/// Whether or not the user needs to link their account via SSO due to an organization setting.
#[serde(default)]
pub needs_sso_link: bool,
/// What type of anonymous user this user is. May be `None` if they are not anonymous.
#[serde(default)]
pub anonymous_user_type: Option<AnonymousUserType>,
#[serde(default)]
pub linked_at: Option<ServerTimestamp>,
#[serde(default)]
pub personal_object_limits: Option<PersonalObjectLimits>,
/// Whether or not this user is on what we consider a "work" domain.
#[serde(default)]
pub is_on_work_domain: bool,
}
#[derive(Debug, thiserror::Error)]
pub enum UserPersistenceError {
/// The persisted user was not successfully read from or written to disk.
#[error("secure storage error")]
SecureStorageError(#[from] secure_storage::Error),
/// The persisted user on disk could not be decoded into a valid struct.
#[error("failed to serialize or deserialize a PersistedUser struct")]
SerializationError(#[from] serde_json::Error),
}
impl PersistedUser {
pub fn from_secure_storage(_ctx: &AppContext) -> Result<PersistedUser, UserPersistenceError> {
Err(UserPersistenceError::SecureStorageError(
secure_storage::Error::NotFound,
))
}
pub fn write_to_secure_storage(&self, _ctx: &AppContext) -> Result<(), UserPersistenceError> {
Ok(())
}
pub fn remove_from_secure_storage(_ctx: &AppContext) -> Result<(), UserPersistenceError> {
Ok(())
}
}
#[cfg(test)]
#[path = "persistence_tests.rs"]
mod tests;
@@ -0,0 +1,156 @@
use chrono::DateTime;
use super::PersistedUser;
use crate::UserUid;
use crate::user::{FirebaseAuthTokens, PersonalObjectLimits, UserMetadata};
/// Verifies that the JSON blob format as of March 6, 2026 can be deserialized correctly.
///
/// We must ALWAYS be backwards-compatible with the format here. The inlined JSON string can never change - it represents
/// data serialized on user devices.
#[test]
#[allow(deprecated)]
fn test_deserialize_2026_03_06_persisted_user() {
const BLOB: &str = r#"{"id_token":{"id_token":"test-id-token","refresh_token":"test-refresh-token","expiration_time":"2099-01-01T00:00:00Z"},"refresh_token":"","local_id":"test-uid","email":"test@example.com","display_name":"Test User","photo_url":"https://example.com/photo.jpg","is_onboarded":true,"needs_sso_link":false,"anonymous_user_type":null,"linked_at":null,"personal_object_limits":null,"is_on_work_domain":false}"#;
let user: PersistedUser =
serde_json::from_str(BLOB).expect("2026-03-06 JSON should deserialize");
assert_eq!(user.auth_tokens.id_token, "test-id-token");
assert_eq!(user.auth_tokens.refresh_token, "test-refresh-token");
assert_eq!(user.refresh_token, "");
assert_eq!(user.local_id.as_str(), "test-uid");
assert_eq!(user.metadata.email, "test@example.com");
assert_eq!(user.metadata.display_name.as_deref(), Some("Test User"));
assert_eq!(
user.metadata.photo_url.as_deref(),
Some("https://example.com/photo.jpg")
);
assert!(user.is_onboarded);
assert!(!user.needs_sso_link);
assert_eq!(user.anonymous_user_type, None);
assert_eq!(user.linked_at, None);
assert!(user.personal_object_limits.is_none());
assert!(!user.is_on_work_domain);
}
/// Verifies that serializing a PersistedUser produces the expected JSON string.
///
/// If this test fails, it means the serialization format has changed.
/// You should:
/// 1. Add a new dated deserialization test (see [`test_deserialize_2026_03_06_persisted_user`])
/// 2. Update the serialization test to match the new format
#[test]
#[allow(deprecated)]
fn test_serialize_persisted_user() {
const EXPECTED_BLOB: &str = r#"{"id_token":{"id_token":"test-id-token","refresh_token":"test-refresh-token","expiration_time":"2099-01-01T00:00:00Z"},"refresh_token":"","local_id":"test-uid","email":"test@example.com","display_name":"Test User","photo_url":"https://example.com/photo.jpg","is_onboarded":true,"needs_sso_link":false,"anonymous_user_type":null,"linked_at":null,"personal_object_limits":{"env_var_limit":10,"notebook_limit":20,"workflow_limit":30},"is_on_work_domain":false}"#;
let expiration_time = DateTime::parse_from_rfc3339("2099-01-01T00:00:00+00:00")
.expect("should parse expiration datetime");
let user = PersistedUser {
auth_tokens: FirebaseAuthTokens {
id_token: "test-id-token".to_string(),
refresh_token: "test-refresh-token".to_string(),
expiration_time,
},
refresh_token: String::new(),
local_id: UserUid::new("test-uid"),
metadata: UserMetadata {
email: "test@example.com".to_string(),
display_name: Some("Test User".to_string()),
photo_url: Some("https://example.com/photo.jpg".to_string()),
},
is_onboarded: true,
needs_sso_link: false,
anonymous_user_type: None,
linked_at: None,
personal_object_limits: Some(PersonalObjectLimits {
env_var_limit: 10,
notebook_limit: 20,
workflow_limit: 30,
}),
is_on_work_domain: false,
};
let serialized = serde_json::to_string(&user).expect("serialization should succeed");
assert_eq!(serialized, EXPECTED_BLOB);
}
/// Test serializing and deserializing persisted user data.
/// See galaxyui_extras::secure_storage::linux_test.rs for Linux-specific tests.
#[cfg(target_os = "windows")]
#[cfg_attr(windows, ignore = "passes locally but not in CI on Windows")]
#[test]
#[allow(deprecated)]
fn test_windows_user_persistence() {
use chrono::Local;
use galaxy_core::channel::ChannelState;
use galaxyui_core::App;
use galaxyui_extras::secure_storage;
App::test((), |mut app| async move {
app.update(|ctx| {
secure_storage::register_with_dir(
ChannelState::data_domain().as_str(),
galaxy_core::paths::state_dir(),
ctx,
);
});
let local_time = Local::now();
let tokens = FirebaseAuthTokens {
id_token: String::from("This is an ID token."),
refresh_token: String::from("This is a refresh token."),
expiration_time: local_time.with_timezone(local_time.offset())
+ chrono::Duration::days(365),
};
let persisted_user = PersistedUser {
auth_tokens: tokens.clone(),
refresh_token: String::new(),
local_id: UserUid::new("test_uid"),
metadata: UserMetadata {
email: "test@test.com".to_string(),
display_name: Some(String::from("abcdef")),
photo_url: Some(String::from("some-photo-url")),
},
is_onboarded: true,
needs_sso_link: false,
anonymous_user_type: None,
linked_at: None,
personal_object_limits: None,
is_on_work_domain: false,
};
app.update(|ctx| {
// Write the test user to secure storage.
let write = persisted_user.write_to_secure_storage(ctx);
match &write {
Ok(()) => {}
Err(err) => {
println!("{err:?}");
}
}
assert!(write.is_ok());
// Read the persisted user back and ensure the fields match.
let stored = PersistedUser::from_secure_storage(ctx).unwrap();
assert_eq!(stored.auth_tokens.id_token, tokens.id_token);
assert_eq!(stored.auth_tokens.refresh_token, tokens.refresh_token);
assert_eq!(stored.auth_tokens.expiration_time, tokens.expiration_time);
assert_eq!(
stored.metadata.display_name,
persisted_user.metadata.display_name
);
assert_eq!(stored.metadata.email, persisted_user.metadata.email);
assert_eq!(stored.metadata.photo_url, persisted_user.metadata.photo_url);
// Remove the user from secure storage.
assert!(PersistedUser::remove_from_secure_storage(ctx).is_ok());
// Attempt to read a user back, which should fail.
let empty_user = PersistedUser::from_secure_storage(ctx);
assert!(empty_user.is_err());
});
});
}