- 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)
153 lines
5.0 KiB
Rust
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,
|
|
})
|
|
}
|
|
}
|