mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
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:
3
apps/aether-gateway/src/state/runtime/auth/mod.rs
Normal file
3
apps/aether-gateway/src/state/runtime/auth/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod sessions;
|
||||
mod user_lifecycle;
|
||||
mod user_provisioning;
|
||||
254
apps/aether-gateway/src/state/runtime/auth/sessions.rs
Normal file
254
apps/aether-gateway/src/state/runtime/auth/sessions.rs
Normal 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()))
|
||||
}
|
||||
}
|
||||
475
apps/aether-gateway/src/state/runtime/auth/user_lifecycle.rs
Normal file
475
apps/aether-gateway/src/state/runtime/auth/user_lifecycle.rs
Normal 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()))
|
||||
}
|
||||
}
|
||||
271
apps/aether-gateway/src/state/runtime/auth/user_provisioning.rs
Normal file
271
apps/aether-gateway/src/state/runtime/auth/user_provisioning.rs
Normal 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()))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user