refactor: 拆分 gateway 单体为独立 crate,新增 systemd 部署方案

将 gateway 内部的 model-fetch、provider-transport、scheduler-core、
usage-runtime、video-tasks-core 模块提取为独立 crate;重构 gateway
内部模块结构(state/router/cache/data/query 等);移除大量遗留模块
文件;新增 systemd 二进制部署骨架及相关文档;更新前端 usage 相关
API 和组件。
This commit is contained in:
fawney19
2026-04-05 20:23:16 +08:00
parent cbc811f6ce
commit 763ff03a7b
777 changed files with 42659 additions and 21469 deletions

View File

@@ -0,0 +1,3 @@
mod sessions;
mod user_lifecycle;
mod user_provisioning;

View File

@@ -0,0 +1,254 @@
use crate::{AppState, GatewayError};
impl AppState {
pub(crate) async fn find_user_session(
&self,
user_id: &str,
session_id: &str,
) -> Result<Option<crate::data::state::StoredUserSessionRecord>, GatewayError>
{
#[cfg(test)]
if let Some(store) = self.auth_session_store.as_ref() {
let key = format!("{user_id}:{session_id}");
return Ok(store
.lock()
.expect("auth session store should lock")
.get(&key)
.cloned());
}
self.data
.find_user_session(user_id, session_id)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn list_user_sessions(
&self,
user_id: &str,
) -> Result<Vec<crate::data::state::StoredUserSessionRecord>, GatewayError>
{
#[cfg(test)]
if let Some(store) = self.auth_session_store.as_ref() {
let prefix = format!("{user_id}:");
let now = chrono::Utc::now();
let mut sessions = store
.lock()
.expect("auth session store should lock")
.iter()
.filter(|(key, _)| key.starts_with(&prefix))
.map(|(_, session)| session.clone())
.filter(|session| !session.is_revoked() && !session.is_expired(now))
.collect::<Vec<_>>();
sessions.sort_by(|left, right| {
right
.last_seen_at
.cmp(&left.last_seen_at)
.then_with(|| right.created_at.cmp(&left.created_at))
});
return Ok(sessions);
}
self.data
.list_user_sessions(user_id)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn touch_user_session(
&self,
user_id: &str,
session_id: &str,
touched_at: chrono::DateTime<chrono::Utc>,
ip_address: Option<&str>,
user_agent: Option<&str>,
) -> Result<bool, GatewayError> {
#[cfg(test)]
if let Some(store) = self.auth_session_store.as_ref() {
let key = format!("{user_id}:{session_id}");
let mut guard = store.lock().expect("auth session store should lock");
if let Some(session) = guard.get_mut(&key) {
session.last_seen_at = Some(touched_at);
if let Some(ip_address) = ip_address {
session.ip_address = Some(ip_address.to_string());
}
if let Some(user_agent) = user_agent {
session.user_agent = Some(user_agent.chars().take(1000).collect());
}
session.updated_at = Some(touched_at);
return Ok(true);
}
return Ok(false);
}
self.data
.touch_user_session(user_id, session_id, touched_at, ip_address, user_agent)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn update_user_session_device_label(
&self,
user_id: &str,
session_id: &str,
device_label: &str,
updated_at: chrono::DateTime<chrono::Utc>,
) -> Result<bool, GatewayError> {
#[cfg(test)]
if let Some(store) = self.auth_session_store.as_ref() {
let key = format!("{user_id}:{session_id}");
let mut guard = store.lock().expect("auth session store should lock");
if let Some(session) = guard.get_mut(&key) {
session.device_label = Some(device_label.chars().take(120).collect());
session.updated_at = Some(updated_at);
return Ok(true);
}
return Ok(false);
}
self.data
.update_user_session_device_label(user_id, session_id, device_label, updated_at)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn create_user_session(
&self,
session: crate::data::state::StoredUserSessionRecord,
) -> Result<Option<crate::data::state::StoredUserSessionRecord>, GatewayError>
{
#[cfg(test)]
if let Some(store) = self.auth_session_store.as_ref() {
let now = session
.created_at
.or(session.updated_at)
.or(session.last_seen_at)
.unwrap_or_else(chrono::Utc::now);
let mut guard = store.lock().expect("auth session store should lock");
for existing in guard.values_mut() {
if existing.user_id == session.user_id
&& existing.client_device_id == session.client_device_id
&& !existing.is_revoked()
&& !existing.is_expired(now)
{
existing.revoked_at = Some(now);
existing.revoke_reason = Some("replaced_by_new_login".to_string());
existing.updated_at = Some(now);
}
}
guard.insert(
format!("{}:{}", session.user_id, session.id),
session.clone(),
);
return Ok(Some(session));
}
self.data
.create_user_session(&session)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn rotate_user_session_refresh_token(
&self,
user_id: &str,
session_id: &str,
previous_refresh_token_hash: &str,
next_refresh_token_hash: &str,
rotated_at: chrono::DateTime<chrono::Utc>,
expires_at: chrono::DateTime<chrono::Utc>,
ip_address: Option<&str>,
user_agent: Option<&str>,
) -> Result<bool, GatewayError> {
#[cfg(test)]
if let Some(store) = self.auth_session_store.as_ref() {
let key = format!("{user_id}:{session_id}");
let mut guard = store.lock().expect("auth session store should lock");
if let Some(session) = guard.get_mut(&key) {
session.prev_refresh_token_hash = Some(previous_refresh_token_hash.to_string());
session.refresh_token_hash = next_refresh_token_hash.to_string();
session.rotated_at = Some(rotated_at);
session.expires_at = Some(expires_at);
session.last_seen_at = Some(rotated_at);
if let Some(ip_address) = ip_address {
session.ip_address = Some(ip_address.to_string());
}
if let Some(user_agent) = user_agent {
session.user_agent = Some(user_agent.chars().take(1000).collect());
}
session.updated_at = Some(rotated_at);
return Ok(true);
}
return Ok(false);
}
self.data
.rotate_user_session_refresh_token(
user_id,
session_id,
previous_refresh_token_hash,
next_refresh_token_hash,
rotated_at,
expires_at,
ip_address,
user_agent,
)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn revoke_user_session(
&self,
user_id: &str,
session_id: &str,
revoked_at: chrono::DateTime<chrono::Utc>,
reason: &str,
) -> Result<bool, GatewayError> {
#[cfg(test)]
if let Some(store) = self.auth_session_store.as_ref() {
let key = format!("{user_id}:{session_id}");
let mut guard = store.lock().expect("auth session store should lock");
if let Some(session) = guard.get_mut(&key) {
session.revoked_at = Some(revoked_at);
session.revoke_reason = Some(reason.chars().take(100).collect());
session.updated_at = Some(revoked_at);
return Ok(true);
}
return Ok(false);
}
self.data
.revoke_user_session(user_id, session_id, revoked_at, reason)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn revoke_all_user_sessions(
&self,
user_id: &str,
revoked_at: chrono::DateTime<chrono::Utc>,
reason: &str,
) -> Result<u64, GatewayError> {
#[cfg(test)]
if let Some(store) = self.auth_session_store.as_ref() {
let prefix = format!("{user_id}:");
let mut revoked = 0_u64;
let mut guard = store.lock().expect("auth session store should lock");
for (key, session) in guard.iter_mut() {
if !key.starts_with(&prefix) || session.revoked_at.is_some() {
continue;
}
session.revoked_at = Some(revoked_at);
session.revoke_reason = Some(reason.chars().take(100).collect());
session.updated_at = Some(revoked_at);
revoked += 1;
}
return Ok(revoked);
}
self.data
.revoke_all_user_sessions(user_id, revoked_at, reason)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
}

View File

@@ -0,0 +1,475 @@
use crate::{AppState, GatewayError};
impl AppState {
pub(crate) async fn find_user_auth_by_id(
&self,
user_id: &str,
) -> Result<Option<aether_data::repository::users::StoredUserAuthRecord>, GatewayError> {
#[cfg(test)]
if let Some(store) = self.auth_user_store.as_ref() {
if let Some(user) = store
.lock()
.expect("auth user store should lock")
.get(user_id)
.cloned()
{
return Ok(Some(user));
}
}
self.data
.find_user_auth_by_id(user_id)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn find_user_auth_by_identifier(
&self,
identifier: &str,
) -> Result<Option<aether_data::repository::users::StoredUserAuthRecord>, GatewayError> {
#[cfg(test)]
if let Some(store) = self.auth_user_store.as_ref() {
let identifier = identifier.trim();
if !identifier.is_empty() {
if let Some(user) = store
.lock()
.expect("auth user store should lock")
.values()
.find(|user| {
user.username == identifier || user.email.as_deref() == Some(identifier)
})
.cloned()
{
return Ok(Some(user));
}
}
}
self.data
.find_user_auth_by_identifier(identifier)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn is_other_user_auth_email_taken(
&self,
email: &str,
user_id: &str,
) -> Result<bool, GatewayError> {
#[cfg(test)]
if let Some(store) = self.auth_user_store.as_ref() {
if store
.lock()
.expect("auth user store should lock")
.values()
.any(|user| user.id != user_id && user.email.as_deref() == Some(email))
{
return Ok(true);
}
}
self.data
.is_other_user_auth_email_taken(email, user_id)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn is_other_user_auth_username_taken(
&self,
username: &str,
user_id: &str,
) -> Result<bool, GatewayError> {
#[cfg(test)]
if let Some(store) = self.auth_user_store.as_ref() {
if store
.lock()
.expect("auth user store should lock")
.values()
.any(|user| user.id != user_id && user.username == username)
{
return Ok(true);
}
}
self.data
.is_other_user_auth_username_taken(username, user_id)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn update_local_auth_user_profile(
&self,
user_id: &str,
email: Option<String>,
username: Option<String>,
) -> Result<Option<aether_data::repository::users::StoredUserAuthRecord>, GatewayError> {
#[cfg(test)]
if let Some(store) = self.auth_user_store.as_ref() {
let existing = {
store
.lock()
.expect("auth user store should lock")
.get(user_id)
.cloned()
};
let existing = match existing {
Some(user) => Some(user),
None => self
.data
.find_user_auth_by_id(user_id)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?,
};
let Some(mut user) = existing else {
return Ok(None);
};
if let Some(email) = email {
user.email = Some(email);
}
if let Some(username) = username {
user.username = username;
}
store
.lock()
.expect("auth user store should lock")
.insert(user.id.clone(), user.clone());
return Ok(Some(user));
}
self.data
.update_local_auth_user_profile(user_id, email, username)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn update_local_auth_user_password_hash(
&self,
user_id: &str,
password_hash: String,
updated_at: chrono::DateTime<chrono::Utc>,
) -> Result<Option<aether_data::repository::users::StoredUserAuthRecord>, GatewayError> {
#[cfg(test)]
if let Some(store) = self.auth_user_store.as_ref() {
let existing = {
store
.lock()
.expect("auth user store should lock")
.get(user_id)
.cloned()
};
let existing = match existing {
Some(user) => Some(user),
None => self
.data
.find_user_auth_by_id(user_id)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?,
};
let Some(mut user) = existing else {
return Ok(None);
};
user.password_hash = Some(password_hash);
store
.lock()
.expect("auth user store should lock")
.insert(user.id.clone(), user.clone());
return Ok(Some(user));
}
self.data
.update_local_auth_user_password_hash(user_id, password_hash, updated_at)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn create_local_auth_user(
&self,
email: Option<String>,
email_verified: bool,
username: String,
password_hash: String,
) -> Result<Option<aether_data::repository::users::StoredUserAuthRecord>, GatewayError> {
#[cfg(test)]
if let Some(store) = self.auth_user_store.as_ref() {
let now = chrono::Utc::now();
let user = aether_data::repository::users::StoredUserAuthRecord::new(
uuid::Uuid::new_v4().to_string(),
email,
email_verified,
username,
Some(password_hash),
"user".to_string(),
"local".to_string(),
None,
None,
None,
true,
false,
Some(now),
None,
)
.map_err(|err| GatewayError::Internal(err.to_string()))?;
store
.lock()
.expect("auth user store should lock")
.insert(user.id.clone(), user.clone());
return Ok(Some(user));
}
self.data
.create_local_auth_user(email, email_verified, username, password_hash)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn create_local_auth_user_with_settings(
&self,
email: Option<String>,
email_verified: bool,
username: String,
password_hash: String,
role: String,
allowed_providers: Option<Vec<String>>,
allowed_api_formats: Option<Vec<String>>,
allowed_models: Option<Vec<String>>,
rate_limit: Option<i32>,
) -> Result<Option<aether_data::repository::users::StoredUserAuthRecord>, GatewayError> {
#[cfg(test)]
if let Some(store) = self.auth_user_store.as_ref() {
let now = chrono::Utc::now();
let user = aether_data::repository::users::StoredUserAuthRecord::new(
uuid::Uuid::new_v4().to_string(),
email,
email_verified,
username,
Some(password_hash),
role,
"local".to_string(),
allowed_providers.map(serde_json::Value::from),
allowed_api_formats.map(serde_json::Value::from),
allowed_models.map(serde_json::Value::from),
true,
false,
Some(now),
None,
)
.map_err(|err| GatewayError::Internal(err.to_string()))?;
store
.lock()
.expect("auth user store should lock")
.insert(user.id.clone(), user.clone());
let _ = rate_limit;
return Ok(Some(user));
}
self.data
.create_local_auth_user_with_settings(
email,
email_verified,
username,
password_hash,
role,
allowed_providers,
allowed_api_formats,
allowed_models,
rate_limit,
)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn update_local_auth_user_admin_fields(
&self,
user_id: &str,
role: Option<String>,
allowed_providers_present: bool,
allowed_providers: Option<Vec<String>>,
allowed_api_formats_present: bool,
allowed_api_formats: Option<Vec<String>>,
allowed_models_present: bool,
allowed_models: Option<Vec<String>>,
rate_limit: Option<i32>,
is_active: Option<bool>,
) -> Result<Option<aether_data::repository::users::StoredUserAuthRecord>, GatewayError> {
#[cfg(test)]
if let Some(store) = self.auth_user_store.as_ref() {
let mut guard = store.lock().expect("auth user store should lock");
let Some(user) = guard.get_mut(user_id) else {
return Ok(None);
};
if let Some(role) = role {
user.role = role;
}
if allowed_providers_present {
user.allowed_providers = allowed_providers;
}
if allowed_api_formats_present {
user.allowed_api_formats = allowed_api_formats;
}
if allowed_models_present {
user.allowed_models = allowed_models;
}
if let Some(is_active) = is_active {
user.is_active = is_active;
}
let _ = rate_limit;
return Ok(Some(user.clone()));
}
self.data
.update_local_auth_user_admin_fields(
user_id,
role,
allowed_providers_present,
allowed_providers,
allowed_api_formats_present,
allowed_api_formats,
allowed_models_present,
allowed_models,
rate_limit,
is_active,
)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn touch_auth_user_last_login(
&self,
user_id: &str,
logged_in_at: chrono::DateTime<chrono::Utc>,
) -> Result<bool, GatewayError> {
#[cfg(test)]
if let Some(store) = self.auth_user_store.as_ref() {
let mut guard = store.lock().expect("auth user store should lock");
if let Some(user) = guard.get_mut(user_id) {
user.last_login_at = Some(logged_in_at);
return Ok(true);
}
return Ok(false);
}
self.data
.touch_auth_user_last_login(user_id, logged_in_at)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn delete_local_auth_user(&self, user_id: &str) -> Result<bool, GatewayError> {
#[cfg(test)]
if let Some(store) = self.auth_user_store.as_ref() {
let removed = store
.lock()
.expect("auth user store should lock")
.remove(user_id)
.is_some();
if removed {
if let Some(wallet_store) = self.auth_wallet_store.as_ref() {
wallet_store
.lock()
.expect("auth wallet store should lock")
.retain(|_, wallet| wallet.user_id.as_deref() != Some(user_id));
}
if let Some(session_store) = self.auth_session_store.as_ref() {
let prefix = format!("{user_id}:");
session_store
.lock()
.expect("auth session store should lock")
.retain(|key, _| !key.starts_with(&prefix));
}
}
return Ok(removed);
}
self.data
.delete_local_auth_user(user_id)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn register_local_auth_user(
&self,
email: Option<String>,
email_verified: bool,
username: String,
password_hash: String,
initial_gift_usd: f64,
unlimited: bool,
) -> Result<
Option<(
aether_data::repository::users::StoredUserAuthRecord,
aether_data::repository::wallet::StoredWalletSnapshot,
)>,
GatewayError,
> {
#[cfg(test)]
if let (Some(user_store), Some(wallet_store)) = (
self.auth_user_store.as_ref(),
self.auth_wallet_store.as_ref(),
) {
let now = chrono::Utc::now();
let user = aether_data::repository::users::StoredUserAuthRecord::new(
uuid::Uuid::new_v4().to_string(),
email,
email_verified,
username,
Some(password_hash),
"user".to_string(),
"local".to_string(),
None,
None,
None,
true,
false,
Some(now),
None,
)
.map_err(|err| GatewayError::Internal(err.to_string()))?;
let gift_balance = if unlimited {
0.0
} else {
initial_gift_usd.max(0.0)
};
let wallet = aether_data::repository::wallet::StoredWalletSnapshot::new(
uuid::Uuid::new_v4().to_string(),
Some(user.id.clone()),
None,
0.0,
gift_balance,
if unlimited {
"unlimited".to_string()
} else {
"finite".to_string()
},
"USD".to_string(),
"active".to_string(),
0.0,
0.0,
0.0,
gift_balance,
now.timestamp(),
)
.map_err(|err| GatewayError::Internal(err.to_string()))?;
user_store
.lock()
.expect("auth user store should lock")
.insert(user.id.clone(), user.clone());
wallet_store
.lock()
.expect("auth wallet store should lock")
.insert(wallet.id.clone(), wallet.clone());
return Ok(Some((user, wallet)));
}
self.data
.register_local_auth_user(
email,
email_verified,
username,
password_hash,
initial_gift_usd,
unlimited,
)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
}

View File

@@ -0,0 +1,271 @@
use crate::{AppState, GatewayError};
impl AppState {
pub(crate) async fn read_user_model_capability_settings(
&self,
user_id: &str,
) -> Result<Option<serde_json::Value>, GatewayError> {
#[cfg(test)]
if let Some(store) = self.auth_user_model_capability_store.as_ref() {
if let Some(settings) = store
.lock()
.expect("auth user model capability store should lock")
.get(user_id)
.cloned()
{
return Ok(Some(settings));
}
}
let users = self.list_non_admin_export_users().await?;
Ok(users
.into_iter()
.find(|user| user.id == user_id)
.and_then(|user| user.model_capability_settings))
}
pub(crate) async fn update_user_model_capability_settings(
&self,
user_id: &str,
settings: Option<serde_json::Value>,
) -> Result<Option<serde_json::Value>, GatewayError> {
#[cfg(test)]
if let Some(store) = self.auth_user_model_capability_store.as_ref() {
let mut guard = store
.lock()
.expect("auth user model capability store should lock");
match settings {
Some(value) => {
guard.insert(user_id.to_string(), value.clone());
return Ok(Some(value));
}
None => {
guard.remove(user_id);
return Ok(None);
}
}
}
self.data
.update_user_model_capability_settings(user_id, settings)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn find_active_provider_name(
&self,
provider_id: &str,
) -> Result<Option<String>, GatewayError> {
self.data
.find_active_provider_name(provider_id)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn get_or_create_ldap_auth_user(
&self,
email: String,
username: String,
ldap_dn: Option<String>,
ldap_username: Option<String>,
logged_in_at: chrono::DateTime<chrono::Utc>,
initial_gift_usd: f64,
unlimited: bool,
) -> Result<Option<aether_data::repository::users::StoredUserAuthRecord>, GatewayError> {
#[cfg(test)]
if let (Some(user_store), Some(wallet_store)) = (
self.auth_user_store.as_ref(),
self.auth_wallet_store.as_ref(),
) {
let mut users = user_store.lock().expect("auth user store should lock");
let existing_id = users
.values()
.find(|user| {
user.email.as_deref() == Some(email.as_str())
|| user.username == username
|| ldap_username
.as_deref()
.is_some_and(|value| user.username == value)
})
.map(|user| user.id.clone());
if let Some(existing_id) = existing_id {
let Some(user) = users.get_mut(&existing_id) else {
return Ok(None);
};
if user.is_deleted || !user.is_active {
return Ok(None);
}
if !user.auth_source.eq_ignore_ascii_case("ldap") {
return Ok(None);
}
user.email = Some(email);
user.email_verified = true;
user.last_login_at = Some(logged_in_at);
return Ok(Some(user.clone()));
}
let base_username = ldap_username
.as_deref()
.filter(|value| !value.trim().is_empty())
.unwrap_or(username.as_str())
.trim()
.to_string();
let mut candidate_username = base_username.clone();
while users
.values()
.any(|user| user.username == candidate_username)
{
let suffix = uuid::Uuid::new_v4().simple().to_string();
candidate_username = format!(
"{}_ldap_{}{}",
base_username,
logged_in_at.timestamp(),
&suffix[..4]
);
}
let user = aether_data::repository::users::StoredUserAuthRecord::new(
uuid::Uuid::new_v4().to_string(),
Some(email),
true,
candidate_username,
None,
"user".to_string(),
"ldap".to_string(),
None,
None,
None,
true,
false,
Some(logged_in_at),
Some(logged_in_at),
)
.map_err(|err| GatewayError::Internal(err.to_string()))?;
users.insert(user.id.clone(), user.clone());
drop(users);
let gift_balance = if unlimited {
0.0
} else {
initial_gift_usd.max(0.0)
};
let wallet = aether_data::repository::wallet::StoredWalletSnapshot::new(
uuid::Uuid::new_v4().to_string(),
Some(user.id.clone()),
None,
0.0,
gift_balance,
if unlimited {
"unlimited".to_string()
} else {
"finite".to_string()
},
"USD".to_string(),
"active".to_string(),
0.0,
0.0,
0.0,
gift_balance,
logged_in_at.timestamp(),
)
.map_err(|err| GatewayError::Internal(err.to_string()))?;
wallet_store
.lock()
.expect("auth wallet store should lock")
.insert(wallet.id.clone(), wallet);
let _ = ldap_dn;
return Ok(Some(user));
}
self.data
.get_or_create_ldap_auth_user(
email,
username,
ldap_dn,
ldap_username,
logged_in_at,
initial_gift_usd,
unlimited,
)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn initialize_auth_user_wallet(
&self,
user_id: &str,
initial_gift_usd: f64,
unlimited: bool,
) -> Result<Option<aether_data::repository::wallet::StoredWalletSnapshot>, GatewayError> {
#[cfg(test)]
if let Some(store) = self.auth_wallet_store.as_ref() {
let gift_balance = if unlimited {
0.0
} else {
initial_gift_usd.max(0.0)
};
let now_unix_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64;
let wallet = aether_data::repository::wallet::StoredWalletSnapshot::new(
uuid::Uuid::new_v4().to_string(),
Some(user_id.to_string()),
None,
0.0,
gift_balance,
if unlimited {
"unlimited".to_string()
} else {
"finite".to_string()
},
"USD".to_string(),
"active".to_string(),
0.0,
0.0,
0.0,
gift_balance,
now_unix_secs,
)
.map_err(|err| GatewayError::Internal(err.to_string()))?;
store
.lock()
.expect("auth wallet store should lock")
.insert(wallet.id.clone(), wallet.clone());
return Ok(Some(wallet));
}
self.data
.initialize_auth_user_wallet(user_id, initial_gift_usd, unlimited)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn update_auth_user_wallet_limit_mode(
&self,
user_id: &str,
limit_mode: &str,
) -> Result<Option<aether_data::repository::wallet::StoredWalletSnapshot>, GatewayError> {
#[cfg(test)]
if let Some(store) = self.auth_wallet_store.as_ref() {
let mut guard = store.lock().expect("auth wallet store should lock");
let Some((wallet_id, wallet)) = guard
.iter_mut()
.find(|(_, wallet)| wallet.user_id.as_deref() == Some(user_id))
else {
return Ok(None);
};
let _ = wallet_id;
wallet.limit_mode = limit_mode.to_string();
wallet.updated_at_unix_secs = chrono::Utc::now().timestamp().max(0) as u64;
return Ok(Some(wallet.clone()));
}
self.data
.update_auth_user_wallet_limit_mode(user_id, limit_mode)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
}