gitdataai/libs/service/user/user_activity.rs

176 lines
6.1 KiB
Rust

use crate::AppService;
use crate::error::AppError;
use chrono::{DateTime, Utc};
use models::projects::{project, project_activity};
use models::users::{user, user_activity_log};
use sea_orm::*;
use serde::{Deserialize, Serialize};
use session::Session;
use utoipa::ToSchema;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct UserActivityItem {
pub id: i64,
pub activity_type: String, // "auth" | "project"
pub action: String,
pub title: String,
pub resource_type: Option<String>,
pub resource_name: Option<String>,
pub metadata: Option<serde_json::Value>,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct UserActivityResponse {
pub items: Vec<UserActivityItem>,
pub total: u64,
pub page: u64,
pub per_page: u64,
}
#[derive(Debug, Clone, Deserialize, Serialize, utoipa::IntoParams)]
pub struct UserActivityQuery {
pub page: Option<u64>,
pub per_page: Option<u64>,
}
impl AppService {
pub async fn get_user_activity(
&self,
context: Session,
username: String,
query: UserActivityQuery,
) -> Result<UserActivityResponse, AppError> {
let target_user = user::Entity::find()
.filter(user::Column::Username.eq(&username))
.one(&self.db)
.await?
.ok_or(AppError::UserNotFound)?;
let current_uid = context.user();
let is_owner = current_uid
.map(|uid| uid == target_user.uid)
.unwrap_or(false);
let page = std::cmp::Ord::max(query.page.unwrap_or(1), 1);
let per_page = std::cmp::Ord::min(std::cmp::Ord::max(query.per_page.unwrap_or(20), 1), 100);
let offset = (page - 1) * per_page;
// User's auth activity log entries
let auth_logs: Vec<(i64, String, DateTime<Utc>, serde_json::Value)> =
user_activity_log::Entity::find()
.filter(user_activity_log::Column::UserUid.eq(target_user.uid))
.order_by_desc(user_activity_log::Column::CreatedAt)
.select_only()
.column(user_activity_log::Column::Id)
.column(user_activity_log::Column::Action)
.column(user_activity_log::Column::CreatedAt)
.column(user_activity_log::Column::Details)
.into_tuple()
.all(&self.db)
.await?;
// User's project activity (where user is the actor)
let proj_activity = project_activity::Entity::find()
.filter(project_activity::Column::Actor.eq(target_user.uid))
.order_by_desc(project_activity::Column::CreatedAt)
.all(&self.db)
.await?;
// Privacy filter: non-owners only see public project activity
let proj_ids: Vec<Uuid> = proj_activity.iter().map(|a| a.project).collect();
let proj_map: std::collections::HashMap<Uuid, project::Model> = project::Entity::find()
.filter(project::Column::Id.is_in(proj_ids))
.all(&self.db)
.await?
.into_iter()
.map(|p| (p.id, p))
.collect();
// Build combined activity list
let mut items: Vec<UserActivityItem> = Vec::new();
// Auth events
for (id, action, created_at, metadata) in auth_logs {
let title = format_action_title(&action);
items.push(UserActivityItem {
id,
activity_type: "auth".to_string(),
action,
title,
resource_type: None,
resource_name: None,
metadata: if metadata != serde_json::Value::Null {
Some(metadata)
} else {
None
},
created_at,
});
}
// Project events
for activity in proj_activity {
// Privacy filter
if !is_owner {
if let Some(proj) = proj_map.get(&activity.project) {
if proj.is_public == false {
continue;
}
} else {
continue;
}
}
items.push(UserActivityItem {
id: activity.id,
activity_type: "project".to_string(),
action: activity.event_type,
title: activity.title,
resource_type: Some("project".to_string()),
resource_name: proj_map.get(&activity.project).map(|p| p.name.clone()),
metadata: activity.metadata,
created_at: activity.created_at,
});
}
// Sort by created_at DESC
items.sort_by(|a, b| b.created_at.cmp(&a.created_at));
let total = items.len() as u64;
let page_items: Vec<UserActivityItem> = items
.into_iter()
.skip(offset as usize)
.take(per_page as usize)
.collect();
Ok(UserActivityResponse {
items: page_items,
total,
page,
per_page,
})
}
}
fn format_action_title(action: &str) -> String {
match action {
"login" => "Signed in".to_string(),
"logout" => "Signed out".to_string(),
"register" => "Created account".to_string(),
"password_change" => "Changed password".to_string(),
"password_reset" => "Reset password".to_string(),
"2fa_enabled" => "Enabled 2FA".to_string(),
"2fa_disabled" => "Disabled 2FA".to_string(),
"ssh_key_add" => "Added SSH key".to_string(),
"ssh_key_delete" => "Removed SSH key".to_string(),
"ssh_key_update" => "Updated SSH key".to_string(),
"ssh_key_revoke" => "Revoked SSH key".to_string(),
"access_key_create" => "Created access token".to_string(),
"access_key_delete" => "Deleted access token".to_string(),
"avatar_upload" => "Updated avatar".to_string(),
"profile_update" => "Updated profile".to_string(),
_ => format!("Activity: {}", action),
}
}