gitdataai/libs/service/user/projects.rs

205 lines
6.9 KiB
Rust

use crate::AppService;
use crate::error::AppError;
use chrono::Utc;
use models::projects::{project, project_members};
use models::users::user;
use sea_orm::prelude::*;
use sea_orm::*;
use serde::{Deserialize, Serialize};
use session::Session;
use utoipa::{IntoParams, ToSchema};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct UserProjectInfo {
pub uid: Uuid,
pub name: String,
pub display_name: String,
pub avatar_url: Option<String>,
pub description: Option<String>,
pub is_public: bool,
pub created_at: chrono::DateTime<Utc>,
pub updated_at: chrono::DateTime<Utc>,
pub member_count: i64,
pub is_member: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct UserProjectsResponse {
pub username: String,
pub projects: Vec<UserProjectInfo>,
pub total_count: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, IntoParams)]
pub struct UserProjectsQuery {
pub page: Option<u64>,
pub per_page: Option<u64>,
}
impl AppService {
pub async fn get_user_projects(
&self,
context: Session,
username: String,
query: UserProjectsQuery,
) -> Result<UserProjectsResponse, AppError> {
let target_user = user::Entity::find()
.filter(user::Column::Username.eq(&username))
.one(&self.db)
.await?
.ok_or(AppError::UserNotFound)?;
let current_user_uid = context.user();
let is_owner = current_user_uid
.map(|uid| uid == target_user.uid)
.unwrap_or(false);
let has_admin_privilege = 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;
// Projects where user is the creator
let created_projects: Vec<Uuid> = project::Entity::find()
.filter(project::Column::CreatedBy.eq(target_user.uid))
.select_only()
.column(project::Column::Id)
.into_tuple::<Uuid>()
.all(&self.db)
.await?;
// Projects where user is a member (via invitation)
let member_projects: Vec<Uuid> = project_members::Entity::find()
.filter(project_members::Column::User.eq(target_user.uid))
.select_only()
.column(project_members::Column::Project)
.into_tuple::<Uuid>()
.all(&self.db)
.await?;
// Union + dedup (preserving first occurrence order)
let mut project_ids: Vec<Uuid> = created_projects;
let project_id_set: std::collections::HashSet<&Uuid> = project_ids.iter().collect();
let new_ids: Vec<Uuid> = member_projects
.into_iter()
.filter(|id| !project_id_set.contains(id))
.collect();
project_ids.extend(new_ids);
let total_count = project_ids.len() as u64;
// Paginate
let page_ids: Vec<Uuid> = project_ids
.into_iter()
.skip(offset as usize)
.take(per_page as usize)
.collect();
if page_ids.is_empty() {
return Ok(UserProjectsResponse {
username: target_user.username,
projects: vec![],
total_count,
});
}
let project_list: Vec<project::Model> = project::Entity::find()
.filter(project::Column::Id.is_in(page_ids.clone()))
.all(&self.db)
.await?;
// Preserve the order from project_ids (created projects first, then member projects)
let mut sorted_projects = project_list;
sorted_projects.sort_by(|a, b| {
let a_idx = page_ids
.iter()
.position(|&x| x == a.id)
.unwrap_or(usize::MAX);
let b_idx = page_ids
.iter()
.position(|&x| x == b.id)
.unwrap_or(usize::MAX);
a_idx.cmp(&b_idx)
});
let user_project_memberships: std::collections::HashSet<Uuid> =
if let Some(uid) = current_user_uid {
project_members::Entity::find()
.filter(project_members::Column::User.eq(uid))
.select_only()
.column(project_members::Column::Project)
.into_tuple::<Uuid>()
.all(&self.db)
.await?
.into_iter()
.collect()
} else {
std::collections::HashSet::new()
};
let member_counts: std::collections::HashMap<Uuid, i64> = if !page_ids.is_empty() {
project_members::Entity::find()
.filter(project_members::Column::Project.is_in(page_ids.clone()))
.select_only()
.column_as(project_members::Column::Project, "project_id")
.column_as(project_members::Column::Id.count(), "count")
.group_by(project_members::Column::Project)
.into_tuple::<(Uuid, i64)>()
.all(&self.db)
.await
.unwrap_or_default()
.into_iter()
.collect()
} else {
std::collections::HashMap::new()
};
let mut project_infos: Vec<UserProjectInfo> = Vec::new();
for project in sorted_projects {
// Privacy: non-owners/non-admins only see public projects (member or created)
if !is_owner && !has_admin_privilege && !project.is_public {
continue;
}
let member_count = member_counts.get(&project.id).copied().unwrap_or(0);
let is_member = user_project_memberships.contains(&project.id);
project_infos.push(UserProjectInfo {
uid: project.id,
name: project.name,
display_name: project.display_name,
avatar_url: project.avatar_url,
description: project.description,
is_public: project.is_public,
created_at: project.created_at,
updated_at: project.updated_at,
member_count: member_count as i64,
is_member,
});
}
Ok(UserProjectsResponse {
username: target_user.username,
projects: project_infos,
total_count,
})
}
pub async fn get_current_user_projects(
&self,
context: Session,
query: UserProjectsQuery,
) -> Result<UserProjectsResponse, AppError> {
let user_uid = context.user().ok_or(AppError::Unauthorized)?;
let user = user::Entity::find()
.filter(user::Column::Uid.eq(user_uid))
.one(&self.db)
.await?
.ok_or(AppError::UserNotFound)?;
self.get_user_projects(context, user.username, query).await
}
}