use std::collections::HashMap;
use crate::oauth2::{AccountSearchField, OAuth2Store, ProviderUserId};
use crate::passkey::{CredentialId, CredentialSearchField, PasskeyStore};
use crate::userdb::{User, UserStore};
use super::errors::CoordinationError;
use crate::session::{
SessionId, User as SessionUser, UserId, cleanup_stale_sessions,
delete_session_from_store_by_session_id, get_user_from_session,
};
pub async fn get_all_users(session_id: SessionId) -> Result<Vec<User>, CoordinationError> {
let _admin_user = validate_admin_session(session_id).await?;
UserStore::get_all_users()
.await
.map_err(|e| CoordinationError::Database(e.to_string()))
}
pub async fn get_user(
session_id: SessionId,
user_id: UserId,
) -> Result<Option<User>, CoordinationError> {
let _admin_user = validate_admin_session(session_id).await?;
UserStore::get_user(user_id)
.await
.map_err(|e| CoordinationError::Database(e.to_string()))
}
pub async fn delete_passkey_credential_admin(
session_id: SessionId,
credential_id: CredentialId,
) -> Result<(), CoordinationError> {
let admin_user = validate_admin_session(session_id).await?;
tracing::debug!(
"Admin user: {} is deleting credential with ID: {}",
admin_user.id,
credential_id.as_str()
);
let credential = PasskeyStore::get_credentials_by(CredentialSearchField::CredentialId(
credential_id.clone(),
))
.await?
.into_iter()
.next()
.ok_or_else(|| {
CoordinationError::ResourceNotFound {
resource_type: "Passkey".to_string(),
resource_id: credential_id.as_str().to_string(),
}
.log()
})?;
let credential_id = CredentialId::new(credential.credential_id.clone())
.map_err(|e| CoordinationError::Validation(format!("Invalid credential ID: {e}")))?;
PasskeyStore::delete_credential_by(CredentialSearchField::CredentialId(credential_id)).await?;
tracing::debug!("Successfully deleted credential");
Ok(())
}
pub async fn delete_oauth2_account_admin(
session_id: SessionId,
provider_user_id: ProviderUserId,
) -> Result<(), CoordinationError> {
let admin_user = validate_admin_session(session_id).await?;
tracing::debug!(
"Admin user: {} is deleting OAuth2 account with ID: {}",
admin_user.id,
provider_user_id.as_str()
);
OAuth2Store::delete_oauth2_accounts_by(AccountSearchField::ProviderUserId(
provider_user_id.clone(),
))
.await?;
tracing::info!(
"Successfully deleted OAuth2 account {} for user {}",
provider_user_id.as_str(),
admin_user.id
);
Ok(())
}
pub async fn delete_user_account_admin(
session_id: SessionId,
user_id: UserId,
) -> Result<(), CoordinationError> {
let _admin_user = validate_admin_session(session_id).await?;
let user = UserStore::get_user(user_id.clone()).await?.ok_or_else(|| {
CoordinationError::ResourceNotFound {
resource_type: "User".to_string(),
resource_id: user_id.as_str().to_string(),
}
.log()
})?;
tracing::debug!("Deleting user account: {:#?}", user);
if user.has_admin_privileges() {
let deleted = UserStore::delete_user_if_not_last_admin(user_id)
.await
.map_err(|e| CoordinationError::Database(e.to_string()))?;
if !deleted {
return Err(CoordinationError::Conflict(
"Cannot delete the last admin user".to_string(),
)
.log());
}
} else {
UserStore::delete_user(user_id).await?;
}
Ok(())
}
pub async fn update_user_admin_status(
session_id: SessionId,
user_id: UserId,
is_admin: bool,
) -> Result<User, CoordinationError> {
let _admin_user = validate_admin_session(session_id).await?;
let user = UserStore::get_user(user_id.clone()).await?.ok_or_else(|| {
CoordinationError::ResourceNotFound {
resource_type: "User".to_string(),
resource_id: user_id.as_str().to_string(),
}
.log()
})?;
if !is_admin && user.sequence_number == Some(1) {
return Err(CoordinationError::Conflict("Cannot demote the first user".to_string()).log());
}
if !is_admin && user.has_admin_privileges() {
let demoted = UserStore::demote_user_if_not_last_admin(user_id.clone())
.await
.map_err(|e| CoordinationError::Database(e.to_string()))?;
if !demoted {
return Err(CoordinationError::Conflict(
"Cannot demote the last admin user".to_string(),
)
.log());
}
let user = UserStore::get_user(user_id).await?.ok_or_else(|| {
CoordinationError::Internal("User disappeared after demotion".to_string())
})?;
Ok(user)
} else {
let updated_user = User { is_admin, ..user };
let user = UserStore::upsert_user(updated_user).await?;
Ok(user)
}
}
pub async fn get_all_active_sessions(
session_id: SessionId,
) -> Result<HashMap<String, usize>, CoordinationError> {
let _admin_user = validate_admin_session(session_id).await?;
let users = UserStore::get_all_users()
.await
.map_err(|e| CoordinationError::Database(e.to_string()))?;
let mut result = HashMap::new();
for user in users {
let session_ids = cleanup_stale_sessions(&user.id).await?;
result.insert(user.id, session_ids.len());
}
Ok(result)
}
pub async fn force_logout_user(
session_id: SessionId,
user_id: UserId,
) -> Result<usize, CoordinationError> {
let admin_user = validate_admin_session(session_id).await?;
tracing::info!(
admin_id = %admin_user.id,
target_user_id = %user_id.as_str(),
"Admin forcing logout for user"
);
let session_ids = cleanup_stale_sessions(user_id.as_str()).await?;
let session_count = session_ids.len();
for sid in session_ids {
let session_id = SessionId::new(sid.clone())
.map_err(|e| CoordinationError::Validation(format!("Invalid session ID: {e}")))?;
if let Err(e) = delete_session_from_store_by_session_id(session_id).await {
tracing::warn!(session_id = %sid, error = %e, "Failed to delete session");
}
}
tracing::info!(
admin_id = %admin_user.id,
target_user_id = %user_id.as_str(),
sessions_terminated = session_count,
"User sessions terminated successfully"
);
Ok(session_count)
}
pub(super) async fn validate_admin_session(
session_id: SessionId,
) -> Result<SessionUser, CoordinationError> {
let session_cookie = crate::SessionCookie::new(session_id.as_str().to_string())
.map_err(|_| CoordinationError::Unauthorized.log())?;
let session_user = get_user_from_session(&session_cookie)
.await
.map_err(|_| CoordinationError::Unauthorized.log())?;
if !session_user.has_admin_privileges() {
tracing::debug!(user_id = %session_user.id, "User is not authorized (not an admin)");
return Err(CoordinationError::Unauthorized.log());
}
tracing::debug!(user_id = %session_user.id, "Admin session validated successfully");
Ok(session_user)
}
#[cfg(test)]
mod tests;