diff --git a/ceres/src/api_service/mod.rs b/ceres/src/api_service/mod.rs index d0de94aa8..4f76e340c 100644 --- a/ceres/src/api_service/mod.rs +++ b/ceres/src/api_service/mod.rs @@ -21,7 +21,8 @@ use mercury::{ use tokio::sync::Mutex; use crate::model::git::{ - CreateFileInfo, LatestCommitInfo, TreeBriefItem, TreeCommitItem, TreeHashItem, + CommitBindingInfo, CreateFileInfo, LatestCommitInfo, TreeBriefItem, TreeCommitItem, + TreeHashItem, }; pub mod import_api_service; @@ -104,7 +105,65 @@ pub trait ApiHandler: Send + Sync { )); }; let commit = self.get_tree_relate_commit(tree.id, path).await?; - Ok(commit.into()) + let mut commit_info: LatestCommitInfo = commit.into(); + + // Build commit binding information + commit_info.binding_info = self.build_commit_binding_info(&commit_info.oid).await?; + + Ok(commit_info) + } + + /// Build commit binding information for a given commit SHA + async fn build_commit_binding_info( + &self, + commit_sha: &str, + ) -> Result, GitError> { + let storage = self.get_context(); + let commit_binding_storage = storage.commit_binding_storage(); + let user_storage = storage.user_storage(); + + if let Ok(Some(binding_model)) = commit_binding_storage.find_by_sha(commit_sha).await { + // Get user information if not anonymous + let user_info = + if !binding_model.is_anonymous && binding_model.matched_username.is_some() { + let username = binding_model.matched_username.as_ref().unwrap(); + if let Ok(Some(user)) = user_storage.find_user_by_name(username).await { + Some((user.name.clone(), user.avatar_url.clone())) + } else { + None + } + } else { + None + }; + + let (display_name, avatar_url, is_verified_user) = if binding_model.is_anonymous { + ("Anonymous".to_string(), None, false) + } else if let Some((username, avatar)) = user_info { + (username, Some(avatar), true) + } else { + ( + binding_model + .author_email + .split('@') + .next() + .unwrap_or(&binding_model.author_email) + .to_string(), + None, + false, + ) + }; + + Ok(Some(CommitBindingInfo { + matched_username: binding_model.matched_username, + is_anonymous: binding_model.is_anonymous, + is_verified_user, + display_name, + avatar_url, + author_email: binding_model.author_email, + })) + } else { + Ok(None) + } } async fn get_tree_info(&self, path: &Path) -> Result, GitError> { diff --git a/ceres/src/auth/mod.rs b/ceres/src/auth/mod.rs index 75a5401c3..b4fa0f534 100644 --- a/ceres/src/auth/mod.rs +++ b/ceres/src/auth/mod.rs @@ -15,10 +15,10 @@ pub struct PushUserInfo { pub trait UserAuthExtractor { /// Extract user information from AccessToken async fn extract_user_from_token(&self, access_token: &str) -> Result; - + /// Verify if email belongs to the authenticated user async fn verify_email_ownership(&self, username: &str, email: &str) -> Result; - + /// Extract user information from username (for cases where token is already validated) async fn extract_user_from_username(&self, username: &str) -> Result; } @@ -37,33 +37,42 @@ impl DefaultUserAuthExtractor { impl UserAuthExtractor for DefaultUserAuthExtractor { async fn extract_user_from_token(&self, access_token: &str) -> Result { // Find username by token - let username = self.user_storage.find_user_by_token(access_token).await? + let username = self + .user_storage + .find_user_by_token(access_token) + .await? .ok_or_else(|| MegaError::with_message("Invalid or expired access token"))?; - + self.extract_user_from_username(&username).await } - + async fn extract_user_from_username(&self, username: &str) -> Result { // Get user information - let user = self.user_storage.find_user_by_name(username).await? + let user = self + .user_storage + .find_user_by_name(username) + .await? .ok_or_else(|| MegaError::with_message("User not found"))?; - + // Get user's email collection - for now, use the primary email // TODO: Extend to support multiple emails per user let all_emails = vec![user.email.clone()]; - + Ok(PushUserInfo { username: user.name.clone(), primary_email: user.email.clone(), all_emails, }) } - + async fn verify_email_ownership(&self, username: &str, email: &str) -> Result { // Get user information - let user = self.user_storage.find_user_by_name(username).await? + let user = self + .user_storage + .find_user_by_name(username) + .await? .ok_or_else(|| MegaError::with_message("User not found"))?; - + // Check if email matches primary email // TODO: Extend to check against all user emails when multiple emails are supported Ok(user.email.to_lowercase() == email.to_lowercase()) diff --git a/ceres/src/merge_checker/gpg_signature_checker.rs b/ceres/src/merge_checker/gpg_signature_checker.rs index c9e835d57..ff0c81b5b 100644 --- a/ceres/src/merge_checker/gpg_signature_checker.rs +++ b/ceres/src/merge_checker/gpg_signature_checker.rs @@ -122,9 +122,7 @@ impl GpgSignatureChecker { signature .verify(&public_key, message.as_bytes()) - .map_err(|e| { - MegaError::with_message(format!("Signature verification failed: {e}")) - })?; + .map_err(|e| MegaError::with_message(format!("Signature verification failed: {e}")))?; Ok(()) } diff --git a/ceres/src/model/git.rs b/ceres/src/model/git.rs index 3d4fe6ce7..ebb3dd91c 100644 --- a/ceres/src/model/git.rs +++ b/ceres/src/model/git.rs @@ -64,6 +64,17 @@ pub struct LatestCommitInfo { pub author: UserInfo, pub committer: UserInfo, pub status: String, + pub binding_info: Option, +} + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct CommitBindingInfo { + pub matched_username: Option, + pub is_anonymous: bool, + pub is_verified_user: bool, + pub display_name: String, + pub avatar_url: Option, + pub author_email: String, } impl From for LatestCommitInfo { @@ -84,6 +95,7 @@ impl From for LatestCommitInfo { author, committer, status: "success".to_string(), + binding_info: None, // Will be populated at API layer } } } diff --git a/ceres/src/pack/monorepo.rs b/ceres/src/pack/monorepo.rs index fabbc00ee..8476b0b4c 100644 --- a/ceres/src/pack/monorepo.rs +++ b/ceres/src/pack/monorepo.rs @@ -158,7 +158,9 @@ impl RepoHandler for MonoRepo { } else { String::new() }; - storage.save_entry(&commit_id, entry_list, self.username.clone()).await + storage + .save_entry(&commit_id, entry_list, self.username.clone()) + .await } async fn check_entry(&self, entry: &Entry) -> Result<(), GitError> { diff --git a/ceres/src/protocol/mod.rs b/ceres/src/protocol/mod.rs index 64da688b0..4b0a03b76 100644 --- a/ceres/src/protocol/mod.rs +++ b/ceres/src/protocol/mod.rs @@ -16,7 +16,7 @@ use import_refs::RefCommand; use jupiter::storage::Storage; use repo::Repo; -use crate::auth::{DefaultUserAuthExtractor, UserAuthExtractor, PushUserInfo}; +use crate::auth::{DefaultUserAuthExtractor, PushUserInfo, UserAuthExtractor}; use crate::pack::{import_repo::ImportRepo, monorepo::MonoRepo, RepoHandler}; pub mod import_refs; @@ -237,15 +237,23 @@ impl SmartProtocol { // For test user, create a basic PushUserInfo match self.create_user_auth_extractor() { Ok(user_auth) => { - if let Ok(user_info) = user_auth.extract_user_from_username(username).await { + if let Ok(user_info) = + user_auth.extract_user_from_username(username).await + { self.authenticated_user = Some(user_info); return true; } else { - tracing::warn!("Failed to extract user info for test user: {}", username); + tracing::warn!( + "Failed to extract user info for test user: {}", + username + ); } } Err(e) => { - tracing::warn!("Failed to create user auth extractor for test user: {}", e); + tracing::warn!( + "Failed to create user auth extractor for test user: {}", + e + ); } } return true; @@ -256,16 +264,21 @@ impl SmartProtocol { .check_token(username, token) .await .unwrap_or(false); - + if token_valid { // Extract user information for valid token match self.create_user_auth_extractor() { Ok(user_auth) => { - if let Ok(user_info) = user_auth.extract_user_from_username(username).await { + if let Ok(user_info) = + user_auth.extract_user_from_username(username).await + { self.authenticated_user = Some(user_info); return true; } else { - tracing::warn!("Failed to extract user info for username: {}", username); + tracing::warn!( + "Failed to extract user info for username: {}", + username + ); } } Err(e) => { @@ -273,13 +286,13 @@ impl SmartProtocol { } } } - + return token_valid; } } false } - + /// Create a user authentication extractor fn create_user_auth_extractor(&self) -> Result { let user_storage = self.storage.user_storage(); diff --git a/ceres/src/protocol/smart.rs b/ceres/src/protocol/smart.rs index 12eeaf86a..842e1d027 100644 --- a/ceres/src/protocol/smart.rs +++ b/ceres/src/protocol/smart.rs @@ -422,7 +422,9 @@ impl SmartProtocol { } // Enhanced user matching logic based on authentication status - let (final_user_id, is_anonymous) = if let Some(authenticated_user) = &self.authenticated_user { + let (final_user_id, is_anonymous) = if let Some(authenticated_user) = + &self.authenticated_user + { // User is authenticated - check if commit author email belongs to them match self.create_user_auth_extractor() { Ok(user_auth) => { @@ -438,7 +440,7 @@ impl SmartProtocol { // Author email doesn't belong to authenticated user - try general matching let user_storage = self.storage.user_storage(); let matched_user = user_storage.find_user_by_email(&author_email).await?; - + if let Some(user) = matched_user { (Some(user.name.clone()), false) } else { @@ -451,14 +453,13 @@ impl SmartProtocol { // Log the error from create_user_auth_extractor for debugging tracing::warn!( "Failed to create user auth extractor for commit binding: {}. \ - Falling back to email-based matching.", + Falling back to email-based matching.", e ); - + // Fallback to email-based matching if auth extractor fails let user_storage = self.storage.user_storage(); let matched_user = user_storage.find_user_by_email(&author_email).await?; - if let Some(user) = matched_user { (Some(user.name.clone()), false) } else { diff --git a/docs/api.md b/docs/api.md index 77459f35b..0b4d4ab6b 100644 --- a/docs/api.md +++ b/docs/api.md @@ -105,14 +105,8 @@ This part of the API, prefixed with /api/v1, is primarily for fetching Git raw o ```bash curl -X GET ${MEGA_URL}/api/v1/count-objs?repo_path= ``` - -6. Retrieve commit binding information for associating commits with users - - ```bash - curl -X GET ${MEGA_URL}/api/v1/commits//binding - ``` - -7. Update commit binding to associate a commit with a specific user or mark as anonymous + +6. Update commit binding to associate a commit with a specific user or mark as anonymous ```bash curl -X PUT ${MEGA_URL}/api/v1/commits//binding \ diff --git a/jupiter/callisto/src/mod.rs b/jupiter/callisto/src/mod.rs index 5da24b718..2224ebe4e 100644 --- a/jupiter/callisto/src/mod.rs +++ b/jupiter/callisto/src/mod.rs @@ -5,6 +5,7 @@ pub mod prelude; pub mod access_token; pub mod builds; pub mod check_result; +pub mod commit_auths; pub mod entity_ext; pub mod git_blob; pub mod git_commit; @@ -36,7 +37,6 @@ pub mod notes; pub mod path_check_configs; pub mod raw_blob; pub mod reactions; -pub mod commit_auths; pub mod relay_lfs_info; pub mod relay_node; pub mod relay_nostr_event; diff --git a/jupiter/callisto/src/prelude.rs b/jupiter/callisto/src/prelude.rs index 60b3cd483..b23ecf8cf 100644 --- a/jupiter/callisto/src/prelude.rs +++ b/jupiter/callisto/src/prelude.rs @@ -3,6 +3,7 @@ pub use super::access_token::Entity as AccessToken; pub use super::builds::Entity as Builds; pub use super::check_result::Entity as CheckResult; +pub use super::commit_auths::Entity as CommitAuths; pub use super::git_blob::Entity as GitBlob; pub use super::git_commit::Entity as GitCommit; pub use super::git_issue::Entity as GitIssue; @@ -32,7 +33,6 @@ pub use super::mq_storage::Entity as MqStorage; pub use super::notes::Entity as Notes; pub use super::path_check_configs::Entity as PathCheckConfigs; pub use super::raw_blob::Entity as RawBlob; -pub use super::commit_auths::Entity as CommitAuths; pub use super::reactions::Entity as Reactions; pub use super::relay_lfs_info::Entity as RelayLfsInfo; pub use super::relay_node::Entity as RelayNode; diff --git a/jupiter/src/storage/commit_binding_storage.rs b/jupiter/src/storage/commit_binding_storage.rs index a881fbf52..b4ad8ef5f 100644 --- a/jupiter/src/storage/commit_binding_storage.rs +++ b/jupiter/src/storage/commit_binding_storage.rs @@ -1,5 +1,5 @@ use crate::storage::base_storage::{BaseStorage, StorageConnector}; -use callisto::commit_auths::Column::{CommitSha}; +use callisto::commit_auths::Column::CommitSha; use callisto::commit_auths::{ActiveModel, Entity}; use common::errors::MegaError; use sea_orm::{ diff --git a/jupiter/src/storage/mod.rs b/jupiter/src/storage/mod.rs index 42140bb92..a0db5b0fc 100644 --- a/jupiter/src/storage/mod.rs +++ b/jupiter/src/storage/mod.rs @@ -188,7 +188,6 @@ impl Storage { self.app_service.note_storage.clone() } - pub fn commit_binding_storage(&self) -> CommitBindingStorage { self.app_service.commit_binding_storage.clone() } diff --git a/jupiter/src/storage/mono_storage.rs b/jupiter/src/storage/mono_storage.rs index cf63d25c1..436468216 100644 --- a/jupiter/src/storage/mono_storage.rs +++ b/jupiter/src/storage/mono_storage.rs @@ -12,7 +12,8 @@ use common::config::MonoConfig; use common::errors::MegaError; use common::utils::{generate_id, MEGA_BRANCH_NAME}; use mercury::internal::object::{MegaObjectModel, ObjectTrait}; -use mercury::internal::{object::blob::Blob, object::commit::Commit, pack::entry::Entry}; +use mercury::internal::{object::commit::Commit, pack::entry::Entry}; +use mercury::internal::object::blob::Blob; use crate::storage::base_storage::{BaseStorage, StorageConnector}; use crate::storage::commit_binding_storage::CommitBindingStorage; diff --git a/jupiter/src/tests.rs b/jupiter/src/tests.rs index ab822f6c7..e6ddd5d2e 100644 --- a/jupiter/src/tests.rs +++ b/jupiter/src/tests.rs @@ -15,8 +15,8 @@ use crate::storage::note_storage::NoteStorage; use crate::storage::{ commit_binding_storage::CommitBindingStorage, conversation_storage::ConversationStorage, git_db_storage::GitDbStorage, issue_storage::IssueStorage, lfs_db_storage::LfsDbStorage, - mr_reviewer_storage::MrReviewerStorage, mono_storage::MonoStorage, mr_storage::MrStorage, - raw_db_storage::RawDbStorage,relay_storage::RelayStorage, user_storage::UserStorage, + mono_storage::MonoStorage, mr_reviewer_storage::MrReviewerStorage, mr_storage::MrStorage, + raw_db_storage::RawDbStorage, relay_storage::RelayStorage, user_storage::UserStorage, vault_storage::VaultStorage, }; use crate::storage::{AppService, Storage}; diff --git a/mono/src/api/api_router.rs b/mono/src/api/api_router.rs index 1733df99a..56295ee71 100644 --- a/mono/src/api/api_router.rs +++ b/mono/src/api/api_router.rs @@ -20,10 +20,11 @@ use common::model::CommonResult; use utoipa_axum::{router::OpenApiRouter, routes}; use crate::api::{ - commit::commit_router, conversation::conv_router, gpg::gpg_router, issue::issue_router, - label::label_router, mr::mr_router, notes::note_router, user::user_router, MonoApiServiceState, + commit::commit_router, conversation::conv_router, error::ApiError, gpg::gpg_router, + issue::issue_router, label::label_router, mr::mr_router, notes::note_router, user::user_router, + MonoApiServiceState, }; -use crate::{api::error::ApiError, server::http_server::GIT_TAG}; +use crate::server::http_server::GIT_TAG; pub fn routers() -> OpenApiRouter { OpenApiRouter::new() diff --git a/mono/src/api/commit/commit_router.rs b/mono/src/api/commit/commit_router.rs index ac2bbf570..042732099 100644 --- a/mono/src/api/commit/commit_router.rs +++ b/mono/src/api/commit/commit_router.rs @@ -1,4 +1,4 @@ -use super::model::{CommitBinding, CommitBindingResponse, UserInfo}; +use super::model::CommitBindingResponse; use crate::api::{error::ApiError, MonoApiServiceState}; use crate::server::http_server::GIT_TAG; use axum::{ @@ -6,8 +6,8 @@ use axum::{ Json, }; use common::model::CommonResult; -use utoipa_axum::{router::OpenApiRouter, routes}; use serde::{Deserialize, Serialize}; +use utoipa_axum::{router::OpenApiRouter, routes}; #[derive(Debug, Deserialize, Serialize, utoipa::ToSchema)] pub struct UpdateCommitBindingRequest { @@ -16,101 +16,8 @@ pub struct UpdateCommitBindingRequest { } pub fn routers() -> OpenApiRouter { - OpenApiRouter::new() - .routes(routes!(get_commit_binding)) - .routes(routes!(update_commit_binding)) -} - -/// Get commit binding information by commit SHA -#[utoipa::path( - get, - path = "/commits/{sha}/binding", - params( - ("sha" = String, Path, description = "Git commit SHA hash") - ), - responses( - (status = 200, description = "Get commit binding information successfully", - body = CommonResult, content_type = "application/json"), - (status = 404, description = "Commit binding not found") - ), - tag = GIT_TAG -)] -#[axum::debug_handler] -async fn get_commit_binding( - State(state): State, - Path(sha): Path, -) -> Result>, ApiError> { - let commit_binding_storage = state.storage.commit_binding_storage(); - let user_storage = state.storage.user_storage(); - - match commit_binding_storage.find_by_sha(&sha).await { - Ok(Some(binding_model)) => { - // Try to get user information if not anonymous - let user_info = if !binding_model.is_anonymous && binding_model.matched_username.is_some() { - let username = binding_model.matched_username.as_ref().unwrap(); - if let Ok(Some(user)) = user_storage.find_user_by_name(username).await { - Some(UserInfo { - id: user.id.to_string(), - username: user.name.clone(), - display_name: Some(user.name.clone()), // Use name as display_name (fallback if no separate display name is available) - avatar_url: Some(user.avatar_url.clone()), - email: user.email.clone(), - }) - } else { - None - } - } else { - None - }; - - let binding = CommitBinding { - id: binding_model.id, - commit_sha: binding_model.commit_sha, - author_email: binding_model.author_email.clone(), - matched_username: binding_model.matched_username, - is_anonymous: binding_model.is_anonymous, - matched_at: binding_model.matched_at.map(|dt| dt.and_utc().to_rfc3339()), - created_at: binding_model.created_at.and_utc().to_rfc3339(), - user: user_info.clone(), - }; - - // Prepare display information - let (display_name, avatar_url, is_verified_user) = if binding_model.is_anonymous { - ("Anonymous".to_string(), None, false) - } else if let Some(ref user) = user_info { - ( - user.display_name.clone().unwrap_or(user.username.clone()), - user.avatar_url.clone(), - true - ) - } else { - // Fallback for cases where user is matched but user info is not available - (binding_model.author_email.split('@').next().unwrap_or(&binding_model.author_email).to_string(), None, false) - }; - - Ok(Json(CommonResult::success(Some(CommitBindingResponse { - binding: Some(binding), - display_name, - avatar_url, - is_verified_user, - })))) - } - Ok(None) => Ok(Json(CommonResult::success(Some(CommitBindingResponse { - binding: None, - display_name: "Anonymous".to_string(), - avatar_url: None, - is_verified_user: false, - })))), - Err(e) => { - tracing::error!("Failed to query commit binding for {}: {}", sha, e); - Err(ApiError::from(anyhow::anyhow!( - "Database query failed: {}", - e - ))) - } - } + OpenApiRouter::new().routes(routes!(update_commit_binding)) } - /// Update commit binding information #[utoipa::path( put, @@ -137,39 +44,71 @@ async fn update_commit_binding( let user_storage = state.storage.user_storage(); // First check if commit binding exists - let existing_binding = commit_binding_storage.find_by_sha(&sha).await + let existing_binding = commit_binding_storage + .find_by_sha(&sha) + .await .map_err(|e| ApiError::from(anyhow::anyhow!("Database query failed: {}", e)))?; let author_email = if let Some(ref binding) = existing_binding { binding.author_email.clone() } else { // If no binding exists, we need the author email - this could be passed in request or derived from git - return Err(ApiError::from(anyhow::anyhow!("No existing binding found for commit {}", sha))); + return Err(ApiError::from(anyhow::anyhow!( + "No existing binding found for commit {}", + sha + ))); }; // Validate user if not anonymous if !request.is_anonymous { if let Some(ref username) = request.username { - let user_exists = user_storage.find_user_by_name(username).await + let user_exists = user_storage + .find_user_by_name(username) + .await .map_err(|e| ApiError::from(anyhow::anyhow!("User validation failed: {}", e)))?; - + if user_exists.is_none() { - return Err(ApiError::from(anyhow::anyhow!("User not found: {}", username))); + return Err(ApiError::from(anyhow::anyhow!( + "User not found: {}", + username + ))); } } else { - return Err(ApiError::from(anyhow::anyhow!("Username required when not anonymous"))); + return Err(ApiError::from(anyhow::anyhow!( + "Username required when not anonymous" + ))); } } // Update the binding - commit_binding_storage.upsert_binding( - &sha, - &author_email, - request.username.clone(), - request.is_anonymous, - ).await - .map_err(|e| ApiError::from(anyhow::anyhow!("Failed to update binding: {}", e)))?; + commit_binding_storage + .upsert_binding( + &sha, + &author_email, + request.username.clone(), + request.is_anonymous, + ) + .await + .map_err(|e| ApiError::from(anyhow::anyhow!("Failed to update binding: {}", e)))?; + + // Prepare response with updated information + let (display_name, avatar_url, is_verified_user) = if request.is_anonymous { + ("Anonymous".to_string(), None, false) + } else if let Some(ref username) = request.username { + // Get user info for verified response + if let Ok(Some(user)) = user_storage.find_user_by_name(username).await { + (user.name.clone(), Some(user.avatar_url.clone()), true) + } else { + (username.clone(), None, true) + } + } else { + ("Anonymous".to_string(), None, false) + }; - // Return updated binding information - get_commit_binding(State(state), Path(sha)).await + // Return success response with complete information + Ok(Json(CommonResult::success(Some(CommitBindingResponse { + display_name, + avatar_url, + is_verified_user, + })))) } diff --git a/mono/src/api/commit/model.rs b/mono/src/api/commit/model.rs index 3bcaa5e75..03b1a8777 100644 --- a/mono/src/api/commit/model.rs +++ b/mono/src/api/commit/model.rs @@ -1,31 +1,9 @@ use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -pub struct UserInfo { - pub id: String, - pub username: String, - pub display_name: Option, - pub avatar_url: Option, - pub email: String, -} - -#[derive(Debug, Serialize, Deserialize, ToSchema)] -pub struct CommitBinding { - pub id: String, - pub commit_sha: String, - pub author_email: String, - pub matched_username: Option, - pub is_anonymous: bool, - pub matched_at: Option, - pub created_at: String, - pub user: Option, // Enhanced: Include user object information -} - #[derive(Debug, Serialize, Deserialize, ToSchema)] pub struct CommitBindingResponse { - pub binding: Option, - pub display_name: String, // Enhanced: Ready-to-use display name - pub avatar_url: Option, // Enhanced: Ready-to-use avatar URL - pub is_verified_user: bool, // Enhanced: Whether the user is verified in the system + pub display_name: String, // Ready-to-use display name + pub avatar_url: Option, // Ready-to-use avatar URL + pub is_verified_user: bool, // Whether the user is verified in the system }