use crate::AppService; use crate::error::AppError; use chrono::{DateTime, Utc}; use models::projects::{project, project_follow}; use models::repos::{repo, repo_star}; use models::users::user; use sea_orm::*; use serde::{Deserialize, Serialize}; use session::Session; use utoipa::ToSchema; use uuid::Uuid; #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct RepoStarItem { pub uid: Uuid, pub repo_name: String, pub owner: String, pub description: Option, pub is_private: bool, pub default_branch: String, pub starred_at: DateTime, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct ProjectFollowItem { pub uid: Uuid, pub name: String, pub display_name: String, pub description: Option, pub is_public: bool, pub followed_at: DateTime, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct UserStarsResponse { pub repos: Vec, pub projects: Vec, pub total: u64, } impl AppService { pub async fn get_user_stars( &self, context: Session, username: String, ) -> Result { 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); // Repo stars let stars = repo_star::Entity::find() .filter(repo_star::Column::User.eq(target_user.uid)) .order_by_desc(repo_star::Column::CreatedAt) .all(&self.db) .await?; let repo_ids: Vec = stars.iter().map(|s| s.repo).collect(); let repos: std::collections::HashMap = repo::Entity::find() .filter(repo::Column::Id.is_in(repo_ids.clone())) .all(&self.db) .await? .into_iter() .map(|r| (r.id, r)) .collect(); let project_ids: Vec = repos.values().map(|r| r.project).collect(); let projects_map: std::collections::HashMap = project::Entity::find() .filter(project::Column::Id.is_in(project_ids)) .all(&self.db) .await? .into_iter() .map(|p| (p.id, p)) .collect(); // Build repo items with privacy filter let mut repo_items: Vec = Vec::new(); for star in &stars { if let Some(r) = repos.get(&star.repo) { // Privacy: non-owners can only see public repos if !is_owner && r.is_private { continue; } let owner_username = if let Some(p) = projects_map.get(&r.project) { p.name.clone() } else { String::new() }; repo_items.push(RepoStarItem { uid: r.id, repo_name: r.repo_name.clone(), owner: owner_username, description: r.description.clone(), is_private: r.is_private, default_branch: r.default_branch.clone(), starred_at: star.created_at, }); } } // Project follows let follows = project_follow::Entity::find() .filter(project_follow::Column::User.eq(target_user.uid)) .order_by_desc(project_follow::Column::CreatedAt) .all(&self.db) .await?; let followed_project_ids: Vec = follows.iter().map(|f| f.project).collect(); let followed_projects: std::collections::HashMap = project::Entity::find() .filter(project::Column::Id.is_in(followed_project_ids)) .all(&self.db) .await? .into_iter() .map(|p| (p.id, p)) .collect(); let mut project_items: Vec = Vec::new(); for follow in &follows { if let Some(p) = followed_projects.get(&follow.project) { // Privacy: non-owners can only see public projects if !is_owner && !p.is_public { continue; } project_items.push(ProjectFollowItem { uid: p.id, name: p.name.clone(), display_name: p.display_name.clone(), description: p.description.clone(), is_public: p.is_public, followed_at: follow.created_at, }); } } let total = repo_items.len() as u64 + project_items.len() as u64; Ok(UserStarsResponse { repos: repo_items, projects: project_items, total, }) } }