Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 61 additions & 2 deletions ceres/src/api_service/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Option<CommitBindingInfo>, 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<Vec<TreeBriefItem>, GitError> {
Expand Down
31 changes: 20 additions & 11 deletions ceres/src/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PushUserInfo, MegaError>;

/// Verify if email belongs to the authenticated user
async fn verify_email_ownership(&self, username: &str, email: &str) -> Result<bool, MegaError>;

/// Extract user information from username (for cases where token is already validated)
async fn extract_user_from_username(&self, username: &str) -> Result<PushUserInfo, MegaError>;
}
Expand All @@ -37,33 +37,42 @@ impl DefaultUserAuthExtractor {
impl UserAuthExtractor for DefaultUserAuthExtractor {
async fn extract_user_from_token(&self, access_token: &str) -> Result<PushUserInfo, MegaError> {
// 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<PushUserInfo, MegaError> {
// 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<bool, MegaError> {
// 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())
Expand Down
4 changes: 1 addition & 3 deletions ceres/src/merge_checker/gpg_signature_checker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}
Expand Down
12 changes: 12 additions & 0 deletions ceres/src/model/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,17 @@ pub struct LatestCommitInfo {
pub author: UserInfo,
pub committer: UserInfo,
pub status: String,
pub binding_info: Option<CommitBindingInfo>,
}

#[derive(Serialize, Deserialize, ToSchema)]
pub struct CommitBindingInfo {
pub matched_username: Option<String>,
pub is_anonymous: bool,
pub is_verified_user: bool,
pub display_name: String,
pub avatar_url: Option<String>,
pub author_email: String,
}

impl From<Commit> for LatestCommitInfo {
Expand All @@ -84,6 +95,7 @@ impl From<Commit> for LatestCommitInfo {
author,
committer,
status: "success".to_string(),
binding_info: None, // Will be populated at API layer
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion ceres/src/pack/monorepo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down
31 changes: 22 additions & 9 deletions ceres/src/protocol/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -256,30 +264,35 @@ 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) => {
tracing::warn!("Failed to create user auth extractor: {}", e);
}
}
}

return token_valid;
}
}
false
}

/// Create a user authentication extractor
fn create_user_auth_extractor(&self) -> Result<DefaultUserAuthExtractor, MegaError> {
let user_storage = self.storage.user_storage();
Expand Down
11 changes: 6 additions & 5 deletions ceres/src/protocol/smart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down
10 changes: 2 additions & 8 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<path/to/repo>
```

6. Retrieve commit binding information for associating commits with users

```bash
curl -X GET ${MEGA_URL}/api/v1/commits/<commit_sha>/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/<commit_sha>/binding \
Expand Down
2 changes: 1 addition & 1 deletion jupiter/callisto/src/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion jupiter/callisto/src/prelude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion jupiter/src/storage/commit_binding_storage.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand Down
1 change: 0 additions & 1 deletion jupiter/src/storage/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
3 changes: 2 additions & 1 deletion jupiter/src/storage/mono_storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions jupiter/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
Loading
Loading