gitdataai/libs/service/user/stars.rs
ZhenYi 80e2201b8b feat(user): add Activity, Following, Stars, Security tabs to profile page
- Backend: user_activity service (user_activity_log + project_activity)
- Backend: stars service (repo_star + project_follow)
- Backend: user_get_following_list (with is_following_me mutual check)
- Frontend: Tab navigation on /user/{username} with Overview/Activity/Following/Stars/Security
- Frontend: UserActivity timeline, FollowingList grid, StarsList, SecurityTab (SSH keys + PATs)
2026-04-22 22:39:14 +08:00

153 lines
5.0 KiB
Rust

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<String>,
pub is_private: bool,
pub default_branch: String,
pub starred_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct ProjectFollowItem {
pub uid: Uuid,
pub name: String,
pub display_name: String,
pub description: Option<String>,
pub is_public: bool,
pub followed_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct UserStarsResponse {
pub repos: Vec<RepoStarItem>,
pub projects: Vec<ProjectFollowItem>,
pub total: u64,
}
impl AppService {
pub async fn get_user_stars(
&self,
context: Session,
username: String,
) -> Result<UserStarsResponse, 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);
// 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<Uuid> = stars.iter().map(|s| s.repo).collect();
let repos: std::collections::HashMap<Uuid, repo::Model> = 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<Uuid> = repos.values().map(|r| r.project).collect();
let projects_map: std::collections::HashMap<Uuid, project::Model> = 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<RepoStarItem> = 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<Uuid> = follows.iter().map(|f| f.project).collect();
let followed_projects: std::collections::HashMap<Uuid, project::Model> = 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<ProjectFollowItem> = 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,
})
}
}