From 1c3e51cdeffff5c6a980dd58c11c41d95a9300bf Mon Sep 17 00:00:00 2001 From: "benjamin.747" Date: Tue, 24 Mar 2026 14:59:13 +0800 Subject: [PATCH 1/2] feat: fix migration and add note table --- .github/workflows/base.yml | 42 +-- .github/workflows/mono-engine-deploy.yml | 10 +- .github/workflows/orion-server-deploy.yml | 8 +- ceres/Cargo.toml | 2 - ceres/src/protocol/mod.rs | 58 --- common/src/config/mod.rs | 25 -- config/config.toml | 16 - jupiter/callisto/src/build_targets.rs | 8 - jupiter/callisto/src/mega_cl.rs | 2 +- jupiter/callisto/src/mega_group.rs | 19 +- jupiter/callisto/src/mega_group_member.rs | 17 +- .../callisto/src/mega_resource_permission.rs | 17 +- jupiter/callisto/src/mega_webhook.rs | 25 +- jupiter/callisto/src/mega_webhook_delivery.rs | 19 +- .../callisto/src/mega_webhook_event_type.rs | 2 + jupiter/callisto/src/mod.rs | 2 + jupiter/callisto/src/non_member_note_views.rs | 23 ++ jupiter/callisto/src/note_views.rs | 20 ++ jupiter/callisto/src/notes.rs | 7 +- jupiter/callisto/src/prelude.rs | 1 + jupiter/callisto/src/sea_orm_active_enums.rs | 10 +- jupiter/callisto/src/target_build_status.rs | 166 +-------- .../migration/m20260324_024559_add_notes.rs | 339 ++++++++++++++++++ .../m20260324_033322_fix_migration.rs | 88 +++++ jupiter/src/migration/mod.rs | 4 + jupiter/src/service/webhook_service.rs | 6 +- jupiter/src/storage/note_storage.rs | 10 +- jupiter/src/storage/webhook_storage.rs | 7 +- mono/src/api/oauth/mod.rs | 131 +++++-- mono/src/git_protocol/http.rs | 39 +- mono/tests/common/mod.rs | 6 - orion-server/src/api.rs | 17 +- orion-server/src/lib.rs | 1 + orion-server/src/main.rs | 1 + orion-server/src/scheduler.rs | 4 +- orion-server/src/service/mod.rs | 1 + .../service/target_build_status_service.rs | 90 +++++ 37 files changed, 841 insertions(+), 402 deletions(-) create mode 100644 jupiter/callisto/src/non_member_note_views.rs create mode 100644 jupiter/callisto/src/note_views.rs create mode 100644 jupiter/src/migration/m20260324_024559_add_notes.rs create mode 100644 jupiter/src/migration/m20260324_033322_fix_migration.rs create mode 100644 orion-server/src/service/mod.rs create mode 100644 orion-server/src/service/target_build_status_service.rs diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index f1e3cab6b..95527c10a 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -8,15 +8,18 @@ on: workflow_dispatch: + push: + branches: + - main pull_request: paths-ignore: - - 'config/**' - - 'docker/**' - - 'docs/**' - - 'moon/**' - - 'scripts/**' - - 'toolchains/**' - - '.github/workflows/web-**' + - "config/**" + - "docker/**" + - "docs/**" + - "moon/**" + - "scripts/**" + - "toolchains/**" + - ".github/workflows/web-**" name: Base GitHub Action for Check, Test and Lints @@ -26,7 +29,7 @@ concurrency: jobs: format: - if: ${{ github.repository == 'web3infra-foundation/mega' }} + if: ${{ !(github.repository == 'web3infra-foundation/mega' && github.event_name == 'push') }} name: Rustfmt Check runs-on: ubuntu-latest steps: @@ -44,7 +47,7 @@ jobs: # clippy: - if: ${{ github.repository == 'web3infra-foundation/mega' }} + if: ${{ !(github.repository == 'web3infra-foundation/mega' && github.event_name == 'push') }} name: Clippy Check runs-on: ubuntu-latest env: @@ -66,21 +69,13 @@ jobs: cargo +stable clippy --all-targets --all-features -- -D warnings test: + if: ${{ !(github.repository == 'web3infra-foundation/mega' && github.event_name == 'push') }} name: Full Test - if: ${{ github.repository == 'web3infra-foundation/mega' }} - strategy: - fail-fast: false - matrix: - include: - - os: ubuntu - runs-on: [self-hosted, Linux, X64] - home: /home/github - - runs-on: ${{ matrix.runs-on }} + runs-on: ${{ fromJson(github.repository == 'web3infra-foundation/mega' && '["self-hosted","Linux","X64"]' || '"ubuntu-latest"') }} env: CARGO_TERM_COLOR: always RUSTUP_TOOLCHAIN: stable - HOME: ${{ matrix.home }} + HOME: ${{ github.repository == 'web3infra-foundation/mega' && '/home/github' || '/home/runner' }} steps: - name: Install Redis run: sudo apt-get update && sudo apt-get install -y redis-server @@ -95,13 +90,13 @@ jobs: uses: ./.github/install-dep with: cache-key: sysdeps - platform: ${{ matrix.os }} - self-hosted: true + platform: ubuntu + self-hosted: ${{ github.repository == 'web3infra-foundation/mega' }} use-gtk: false - name: Set up git lfs run: | - echo "GPG_TTY=${{ matrix.os == 'windows' && 'CON' || '$(tty)' }}" >> $GITHUB_ENV + echo "GPG_TTY=$(tty)" >> $GITHUB_ENV git lfs install git config --global user.email "mega@github.com" git config --global user.name "Mega" @@ -114,6 +109,7 @@ jobs: cargo test --manifest-path ceres/Cargo.toml --all-features --no-fail-fast -- --nocapture cargo test --manifest-path vault/Cargo.toml --all-features --no-fail-fast -- --nocapture cargo test --manifest-path saturn/Cargo.toml --all-features --no-fail-fast -- --nocapture + cargo test --manifest-path orion-server/Cargo.toml --all-features --no-fail-fast -- --nocapture # Note: The fuse/scorpio job has been removed as scorpio has been moved # to its own repository: https://github.com/web3infra-foundation/scorpiofs diff --git a/.github/workflows/mono-engine-deploy.yml b/.github/workflows/mono-engine-deploy.yml index 2a2f0a39b..8a8ad71fe 100644 --- a/.github/workflows/mono-engine-deploy.yml +++ b/.github/workflows/mono-engine-deploy.yml @@ -85,13 +85,19 @@ jobs: ECR_IMAGE="$ECR_REGISTRY/${{ env.REGISTRY_ALIAS }}/${{ env.REPOSITORY }}:$IMAGE_TAG" GCP_IMAGE="us-central1-docker.pkg.dev/${{ env.GCP_PROJECT_ID }}/${{ env.REPOSITORY }}:$IMAGE_TAG" + ECR_CACHE_IMAGE="$ECR_REGISTRY/${{ env.REGISTRY_ALIAS }}/${{ env.REPOSITORY }}:buildcache-${ARCH_SUFFIX}" + CACHE_SCOPE="mono-engine-${ARCH_SUFFIX}" echo "ECR_IMAGE=$ECR_IMAGE" echo "GCP_IMAGE=$GCP_IMAGE" + echo "ECR_CACHE_IMAGE=$ECR_CACHE_IMAGE" + echo "CACHE_SCOPE=$CACHE_SCOPE" docker buildx build \ - --cache-from type=gha \ - --cache-to type=gha,mode=max \ + --cache-from type=gha,scope=$CACHE_SCOPE \ + --cache-from type=registry,ref=$ECR_CACHE_IMAGE \ + --cache-to type=gha,mode=max,scope=$CACHE_SCOPE \ + --cache-to type=registry,ref=$ECR_CACHE_IMAGE,mode=max \ --provenance=false \ --sbom=false \ -f ./mono/Dockerfile \ diff --git a/.github/workflows/orion-server-deploy.yml b/.github/workflows/orion-server-deploy.yml index 172f11454..87d2eb229 100644 --- a/.github/workflows/orion-server-deploy.yml +++ b/.github/workflows/orion-server-deploy.yml @@ -79,11 +79,15 @@ jobs: GCP_IMAGE_BASE="us-central1-docker.pkg.dev/${{ env.GCP_PROJECT_ID }}/${{ env.REPOSITORY }}" TAG="${{ env.IMAGE_TAG_BASE }}-$ARCH_SUFFIX" + AWS_CACHE_IMAGE="$AWS_IMAGE_BASE:buildcache-$ARCH_SUFFIX" + CACHE_SCOPE="orion-server-$ARCH_SUFFIX" docker buildx build \ --platform "$PLATFORM" \ - --cache-from type=gha \ - --cache-to type=gha,mode=max \ + --cache-from type=gha,scope=$CACHE_SCOPE \ + --cache-from type=registry,ref=$AWS_CACHE_IMAGE \ + --cache-to type=gha,mode=max,scope=$CACHE_SCOPE \ + --cache-to type=registry,ref=$AWS_CACHE_IMAGE,mode=max \ --provenance=false \ --sbom=false \ -f orion-server/Dockerfile \ diff --git a/ceres/Cargo.toml b/ceres/Cargo.toml index 2852c33d1..51821c53e 100644 --- a/ceres/Cargo.toml +++ b/ceres/Cargo.toml @@ -37,8 +37,6 @@ async-recursion = { workspace = true } rand = { workspace = true } sysinfo = { workspace = true } utoipa = { workspace = true } -base64 = { workspace = true } -http = { workspace = true } regex = { workspace = true } tokio-util = { workspace = true } uuid = { workspace = true, features = ["v4"] } diff --git a/ceres/src/protocol/mod.rs b/ceres/src/protocol/mod.rs index 1cf30cf32..41463bb58 100644 --- a/ceres/src/protocol/mod.rs +++ b/ceres/src/protocol/mod.rs @@ -1,14 +1,12 @@ use core::fmt; use std::{path::PathBuf, str::FromStr, sync::Arc}; -use base64::{engine::general_purpose, prelude::*}; use bellatrix::Bellatrix; use callisto::sea_orm_active_enums::RefTypeEnum; use common::{ errors::{MegaError, ProtocolError}, utils::ZERO_ID, }; -use http::{HeaderMap, HeaderValue}; use import_refs::RefCommand; use jupiter::redis::lock::RedLock; use repo::Repo; @@ -225,62 +223,6 @@ impl SmartProtocol { Ok(Arc::new(res)) } } - - pub fn enable_http_auth(&self, state: &ProtocolApiState) -> bool { - state.storage.config().enable_http_auth() - } - - pub async fn http_auth( - &mut self, - state: &ProtocolApiState, - header: &HeaderMap, - ) -> bool { - for (k, v) in header { - if k == http::header::AUTHORIZATION { - let decoded = general_purpose::STANDARD - .decode( - v.to_str() - .unwrap() - .strip_prefix("Basic ") - .unwrap() - .as_bytes(), - ) - .unwrap(); - let credentials = String::from_utf8(decoded).unwrap_or_default(); - let mut parts = credentials.splitn(2, ':'); - let username = parts.next().unwrap_or(""); - self.username = Some(username.to_owned()); - let token = parts.next().unwrap_or(""); - let auth_config = state.storage.config().authentication.clone(); - if auth_config.enable_test_user - && username == auth_config.test_user_name - && token == auth_config.test_user_token - { - self.authenticated_user = Some(PushUserInfo { - username: username.to_string(), - }); - return true; - } - let token_valid = state - .storage - .user_storage() - .check_token(username, token) - .await - .unwrap_or(false); - - if token_valid { - // Valid token: set minimal authenticated user info - self.authenticated_user = Some(PushUserInfo { - username: username.to_string(), - }); - return true; - } - - return token_valid; - } - } - false - } } #[cfg(test)] diff --git a/common/src/config/mod.rs b/common/src/config/mod.rs index ced7707c7..2f5e28ad2 100644 --- a/common/src/config/mod.rs +++ b/common/src/config/mod.rs @@ -84,7 +84,6 @@ pub struct Config { pub database: DbConfig, pub monorepo: MonoConfig, pub pack: PackConfig, - pub authentication: AuthConfig, pub lfs: LFSConfig, #[serde(default)] pub blame: BlameConfig, @@ -129,7 +128,6 @@ impl Config { database: DbConfig::default(), monorepo: MonoConfig::default(), pack: PackConfig::default(), - authentication: AuthConfig::default(), lfs: LFSConfig::default(), blame: BlameConfig::default(), oauth: OauthConfig::default(), @@ -173,10 +171,6 @@ impl Config { pub fn from_config(config: c::Config) -> Result { config.try_deserialize::() } - - pub fn enable_http_auth(&self) -> bool { - self.authentication.enable_http_auth - } } /// supports braces-delimited variables (i.e. ${foo}) in config. @@ -351,25 +345,6 @@ impl Default for RenameConfig { } } } -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct AuthConfig { - pub enable_http_auth: bool, - pub enable_test_user: bool, - pub test_user_name: String, - pub test_user_token: String, -} - -impl Default for AuthConfig { - fn default() -> Self { - Self { - enable_http_auth: false, - enable_test_user: false, - test_user_name: String::from("mega"), - test_user_token: String::from("mega"), - } - } -} - #[derive(Serialize, Deserialize, Debug, Clone)] pub struct PackConfig { #[serde(deserialize_with = "string_or_usize")] diff --git a/config/config.toml b/config/config.toml index 1698efa27..6fdb1ce72 100644 --- a/config/config.toml +++ b/config/config.toml @@ -47,22 +47,6 @@ connect_timeout = 3 # Whether to enable SQLx logging sqlx_logging = false -[authentication] -# Support http authentication, login in with github and generate token before push -enable_http_auth = false - -# Enable a test user for debugging and development purposes. -# If set to true, the service allows using a predefined test user for authentication. -enable_test_user = true - -# Specify the name of the test user. -# This is only relevant if `enable_test_user` is set to true. -test_user_name = "mega" - -# Specify the token for the test user. -# This is used for authentication when `enable_test_user` is set to true. -test_user_token = "mega" - [monorepo] ## Only import directory support multi-branch commit and tag, monorepo only support main branch ## Mega treats files under this directory as import repo and other directories as monorepo diff --git a/jupiter/callisto/src/build_targets.rs b/jupiter/callisto/src/build_targets.rs index 51c2b02df..ca41aa2ff 100644 --- a/jupiter/callisto/src/build_targets.rs +++ b/jupiter/callisto/src/build_targets.rs @@ -41,11 +41,3 @@ impl Related for Entity { } impl ActiveModelBehavior for ActiveModel {} -//:path1 //:path2 //:path3 - -//target log: -// - id -// - target id -// - task -// - state -// - createdAt diff --git a/jupiter/callisto/src/mega_cl.rs b/jupiter/callisto/src/mega_cl.rs index d2b6654ad..ca06bcd21 100644 --- a/jupiter/callisto/src/mega_cl.rs +++ b/jupiter/callisto/src/mega_cl.rs @@ -17,12 +17,12 @@ pub struct Model { pub status: MergeStatusEnum, #[sea_orm(column_type = "Text")] pub path: String, - pub base_branch: String, pub from_hash: String, pub to_hash: String, pub created_at: DateTime, pub updated_at: DateTime, pub username: String, + pub base_branch: String, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/jupiter/callisto/src/mega_group.rs b/jupiter/callisto/src/mega_group.rs index ca2738397..a7cb714ce 100644 --- a/jupiter/callisto/src/mega_group.rs +++ b/jupiter/callisto/src/mega_group.rs @@ -16,6 +16,23 @@ pub struct Model { } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} +pub enum Relation { + #[sea_orm(has_many = "super::mega_group_member::Entity")] + MegaGroupMember, + #[sea_orm(has_many = "super::mega_resource_permission::Entity")] + MegaResourcePermission, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::MegaGroupMember.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::MegaResourcePermission.def() + } +} impl ActiveModelBehavior for ActiveModel {} diff --git a/jupiter/callisto/src/mega_group_member.rs b/jupiter/callisto/src/mega_group_member.rs index 5bddc3e4b..bad7e8277 100644 --- a/jupiter/callisto/src/mega_group_member.rs +++ b/jupiter/callisto/src/mega_group_member.rs @@ -14,6 +14,21 @@ pub struct Model { } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} +pub enum Relation { + #[sea_orm( + belongs_to = "super::mega_group::Entity", + from = "Column::GroupId", + to = "super::mega_group::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + MegaGroup, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::MegaGroup.def() + } +} impl ActiveModelBehavior for ActiveModel {} diff --git a/jupiter/callisto/src/mega_resource_permission.rs b/jupiter/callisto/src/mega_resource_permission.rs index cf485ecb0..785f88139 100644 --- a/jupiter/callisto/src/mega_resource_permission.rs +++ b/jupiter/callisto/src/mega_resource_permission.rs @@ -19,6 +19,21 @@ pub struct Model { } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} +pub enum Relation { + #[sea_orm( + belongs_to = "super::mega_group::Entity", + from = "Column::GroupId", + to = "super::mega_group::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + MegaGroup, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::MegaGroup.def() + } +} impl ActiveModelBehavior for ActiveModel {} diff --git a/jupiter/callisto/src/mega_webhook.rs b/jupiter/callisto/src/mega_webhook.rs index 83e708d5e..1556a9d48 100644 --- a/jupiter/callisto/src/mega_webhook.rs +++ b/jupiter/callisto/src/mega_webhook.rs @@ -1,3 +1,5 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; @@ -16,28 +18,23 @@ pub struct Model { pub updated_at: DateTime, } -#[derive(Copy, Clone, Debug, EnumIter)] +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { - WebhookEventTypes, + #[sea_orm(has_many = "super::mega_webhook_delivery::Entity")] + MegaWebhookDelivery, + #[sea_orm(has_many = "super::mega_webhook_event_type::Entity")] + MegaWebhookEventType, } -impl RelationTrait for Relation { - fn def(&self) -> RelationDef { - match self { - Self::WebhookEventTypes => { - Entity::has_many(super::mega_webhook_event_type::Entity).into() - } - } +impl Related for Entity { + fn to() -> RelationDef { + Relation::MegaWebhookDelivery.def() } } impl Related for Entity { fn to() -> RelationDef { - Relation::WebhookEventTypes.def() - } - - fn via() -> Option { - None + Relation::MegaWebhookEventType.def() } } diff --git a/jupiter/callisto/src/mega_webhook_delivery.rs b/jupiter/callisto/src/mega_webhook_delivery.rs index 973d9caaf..fe1f1f78c 100644 --- a/jupiter/callisto/src/mega_webhook_delivery.rs +++ b/jupiter/callisto/src/mega_webhook_delivery.rs @@ -1,3 +1,5 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; @@ -23,6 +25,21 @@ pub struct Model { } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} +pub enum Relation { + #[sea_orm( + belongs_to = "super::mega_webhook::Entity", + from = "Column::WebhookId", + to = "super::mega_webhook::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + MegaWebhook, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::MegaWebhook.def() + } +} impl ActiveModelBehavior for ActiveModel {} diff --git a/jupiter/callisto/src/mega_webhook_event_type.rs b/jupiter/callisto/src/mega_webhook_event_type.rs index 59fdcfa00..2d630a821 100644 --- a/jupiter/callisto/src/mega_webhook_event_type.rs +++ b/jupiter/callisto/src/mega_webhook_event_type.rs @@ -1,3 +1,5 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; diff --git a/jupiter/callisto/src/mod.rs b/jupiter/callisto/src/mod.rs index 57c655469..4a561bafd 100644 --- a/jupiter/callisto/src/mod.rs +++ b/jupiter/callisto/src/mod.rs @@ -55,6 +55,8 @@ pub mod mega_webhook; pub mod mega_webhook_delivery; pub mod mega_webhook_event_type; pub mod merge_queue; +pub mod non_member_note_views; +pub mod note_views; pub mod notes; pub mod orion_tasks; pub mod path_check_configs; diff --git a/jupiter/callisto/src/non_member_note_views.rs b/jupiter/callisto/src/non_member_note_views.rs new file mode 100644 index 000000000..ed4639076 --- /dev/null +++ b/jupiter/callisto/src/non_member_note_views.rs @@ -0,0 +1,23 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "non_member_note_views")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: i64, + pub note_id: i64, + pub user_id: Option, + pub anonymized_ip: String, + #[sea_orm(column_type = "Text", nullable)] + pub user_agent: Option, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/jupiter/callisto/src/note_views.rs b/jupiter/callisto/src/note_views.rs new file mode 100644 index 000000000..dbc20275f --- /dev/null +++ b/jupiter/callisto/src/note_views.rs @@ -0,0 +1,20 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "note_views")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: i64, + pub note_id: i64, + pub user_id: i64, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/jupiter/callisto/src/notes.rs b/jupiter/callisto/src/notes.rs index dd7f5430b..b52090993 100644 --- a/jupiter/callisto/src/notes.rs +++ b/jupiter/callisto/src/notes.rs @@ -6,13 +6,13 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[sea_orm(table_name = "notes")] pub struct Model { - #[sea_orm(primary_key)] + #[sea_orm(primary_key, auto_increment = false)] pub id: i64, #[sea_orm(unique)] pub public_id: String, pub comments_count: i32, pub discarded_at: Option, - pub organization_membership_id: i64, + pub user_id: i64, #[sea_orm(column_type = "Text", nullable)] pub description_html: Option, #[sea_orm(column_type = "Text", nullable)] @@ -22,16 +22,13 @@ pub struct Model { pub title: Option, pub created_at: DateTime, pub updated_at: DateTime, - pub original_project_id: Option, pub original_post_id: Option, pub original_digest_id: Option, pub visibility: i32, pub non_member_views_count: i32, pub resolved_comments_count: Option, - pub project_id: Option, pub last_activity_at: Option, pub content_updated_at: Option, - pub project_permission: i32, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/jupiter/callisto/src/prelude.rs b/jupiter/callisto/src/prelude.rs index 7149aee72..09b192bbf 100644 --- a/jupiter/callisto/src/prelude.rs +++ b/jupiter/callisto/src/prelude.rs @@ -26,6 +26,7 @@ pub use super::{ mega_tag::Entity as MegaTag, mega_tree::Entity as MegaTree, mega_webhook::Entity as MegaWebhook, mega_webhook_delivery::Entity as MegaWebhookDelivery, mega_webhook_event_type::Entity as MegaWebhookEventType, merge_queue::Entity as MergeQueue, + non_member_note_views::Entity as NonMemberNoteViews, note_views::Entity as NoteViews, notes::Entity as Notes, orion_tasks::Entity as OrionTasks, path_check_configs::Entity as PathCheckConfigs, reactions::Entity as Reactions, ssh_keys::Entity as SshKeys, target_build_status::Entity as TargetBuildStatus, diff --git a/jupiter/callisto/src/sea_orm_active_enums.rs b/jupiter/callisto/src/sea_orm_active_enums.rs index d40a0664b..1bc722d83 100644 --- a/jupiter/callisto/src/sea_orm_active_enums.rs +++ b/jupiter/callisto/src/sea_orm_active_enums.rs @@ -289,10 +289,12 @@ pub enum ThreadStatusEnum { #[sea_orm(string_value = "open")] Open, } -#[derive( - Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter, DeriveActiveEnum, Serialize, Deserialize, +#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] +#[sea_orm( + rs_type = "String", + db_type = "Enum", + enum_name = "webhook_event_type_enum" )] -#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")] pub enum WebhookEventTypeEnum { #[sea_orm(string_value = "cl.created")] ClCreated, @@ -306,6 +308,6 @@ pub enum WebhookEventTypeEnum { ClReopened, #[sea_orm(string_value = "cl.comment.created")] ClCommentCreated, - #[sea_orm(string_value = "*")] + #[sea_orm(string_value = "all")] All, } diff --git a/jupiter/callisto/src/target_build_status.rs b/jupiter/callisto/src/target_build_status.rs index a720d7b90..d9735e5ea 100644 --- a/jupiter/callisto/src/target_build_status.rs +++ b/jupiter/callisto/src/target_build_status.rs @@ -1,7 +1,6 @@ //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 -use chrono::Utc; -use sea_orm::{ActiveValue::Set, entity::prelude::*, sea_query::OnConflict}; +use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; use super::sea_orm_active_enums::OrionTargetStatusEnum; @@ -48,166 +47,3 @@ impl Related for Entity { } impl ActiveModelBehavior for ActiveModel {} - -impl Model { - /// Create a new ActiveModel for insertion/upsert - #[allow(clippy::too_many_arguments)] - pub fn new_active_model( - id: Uuid, - task_id: Uuid, - target_package: String, - target_name: String, - target_configuration: String, - category: String, - identifier: String, - action: String, - status: OrionTargetStatusEnum, - ) -> ActiveModel { - let now = Utc::now().into(); - - ActiveModel { - id: Set(id), - task_id: Set(task_id), - target_package: Set(target_package), - target_name: Set(target_name), - target_configuration: Set(target_configuration), - category: Set(category), - identifier: Set(identifier), - action: Set(action), - status: Set(status), - created_at: Set(now), - updated_at: Set(now), - } - } - - /// Batch upsert into the database - /// On conflict (task_id, action) update status and updated_at - pub async fn upsert_batch( - conn: &DatabaseConnection, - models: Vec, - ) -> Result<(), sea_orm::DbErr> { - if models.is_empty() { - return Ok(()); - } - - Entity::insert_many(models) - .on_conflict( - OnConflict::columns([ - Column::TaskId, - Column::TargetPackage, - Column::TargetName, - Column::TargetConfiguration, - Column::Category, - Column::Identifier, - Column::Action, - ]) - .update_columns([Column::Status, Column::UpdatedAt]) - .to_owned(), - ) - .exec(conn) - .await?; - - Ok(()) - } - - /// Delete all targets by task_id - pub async fn delete_by_task_id( - conn: &DatabaseConnection, - task_id: Uuid, - ) -> Result<(), sea_orm::DbErr> { - Entity::delete_many() - .filter(Column::TaskId.eq(task_id)) - .exec(conn) - .await?; - Ok(()) - } - - /// Fetch all targets by task_id - pub async fn fetch_by_task_id( - conn: &DatabaseConnection, - task_id: Uuid, - ) -> Result, sea_orm::DbErr> { - Entity::find() - .filter(Column::TaskId.eq(task_id)) - .all(conn) - .await - } -} - -impl Entity { - /// Insert or update a single target status - #[allow(clippy::too_many_arguments)] - pub async fn upsert_one( - conn: &DatabaseConnection, - id: Uuid, - task_id: Uuid, - target_package: String, - target_name: String, - target_configuration: String, - category: String, - identifier: String, - action: String, - status: OrionTargetStatusEnum, - ) -> Result<(), sea_orm::DbErr> { - let model = Model::new_active_model( - id, - task_id, - target_package, - target_name, - target_configuration, - category, - identifier, - action, - status, - ); - - Model::upsert_batch(conn, vec![model]).await - } - - /// Batch upsert multiple target statuses - pub async fn upsert_batch( - conn: &DatabaseConnection, - models: Vec, - ) -> Result<(), sea_orm::DbErr> { - Model::upsert_batch(conn, models).await - } - - /// Fetch all targets by task_id - pub async fn fetch_by_task_id( - conn: &DatabaseConnection, - task_id: Uuid, - ) -> Result, sea_orm::DbErr> { - Model::fetch_by_task_id(conn, task_id).await - } - - /// Delete all targets by task_id - pub async fn delete_by_task_id( - conn: &DatabaseConnection, - task_id: Uuid, - ) -> Result<(), sea_orm::DbErr> { - Model::delete_by_task_id(conn, task_id).await - } - - /// Update the status of a specific target (by task_id + action) - pub async fn update_status( - conn: &DatabaseConnection, - task_id: Uuid, - action: &str, - status: OrionTargetStatusEnum, - ) -> Result<(), sea_orm::DbErr> { - let now = chrono::Utc::now().into(); - - Entity::update_many() - .set(ActiveModel { - status: Set(status), - updated_at: Set(now), - ..Default::default() - }) - .filter(Column::TaskId.eq(task_id)) - .filter(Column::Action.eq(action)) - .exec(conn) - .await?; - - Ok(()) - } -} diff --git a/jupiter/src/migration/m20260324_024559_add_notes.rs b/jupiter/src/migration/m20260324_024559_add_notes.rs new file mode 100644 index 000000000..c5983220d --- /dev/null +++ b/jupiter/src/migration/m20260324_024559_add_notes.rs @@ -0,0 +1,339 @@ +use sea_orm::DatabaseBackend; +use sea_orm_migration::prelude::*; + +use crate::migration::pk_bigint; + +#[derive(Iden)] +enum Notes { + Table, + Id, + PublicId, + CommentsCount, + DiscardedAt, + UserId, + DescriptionHtml, + DescriptionState, + DescriptionSchemaVersion, + Title, + CreatedAt, + UpdatedAt, + OriginalPostId, + OriginalDigestId, + Visibility, + NonMemberViewsCount, + ResolvedCommentsCount, + LastActivityAt, + ContentUpdatedAt, +} + +#[derive(Iden)] +enum NoteViews { + Table, + Id, + NoteId, + UserId, + CreatedAt, + UpdatedAt, +} + +#[derive(Iden)] +enum NonMemberNoteViews { + Table, + Id, + NoteId, + UserId, + AnonymizedIp, + UserAgent, + CreatedAt, + UpdatedAt, +} + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Notes::Table).if_exists().to_owned()) + .await?; + + manager + .create_table( + Table::create() + .table(Notes::Table) + .if_not_exists() + .col(pk_bigint(Notes::Id)) + .col(ColumnDef::new(Notes::PublicId).string_len(12).not_null()) + .col( + ColumnDef::new(Notes::CommentsCount) + .unsigned() + .not_null() + .default(0), + ) + .col(ColumnDef::new(Notes::DiscardedAt).date_time().null()) + .col(ColumnDef::new(Notes::UserId).big_unsigned().not_null()) + .col(ColumnDef::new(Notes::DescriptionHtml).text()) + .col(ColumnDef::new(Notes::DescriptionState).text()) + .col( + ColumnDef::new(Notes::DescriptionSchemaVersion) + .integer() + .not_null() + .default(0), + ) + .col(ColumnDef::new(Notes::Title).text()) + .col(ColumnDef::new(Notes::CreatedAt).date_time().not_null()) + .col(ColumnDef::new(Notes::UpdatedAt).date_time().not_null()) + .col(ColumnDef::new(Notes::OriginalPostId).big_unsigned().null()) + .col( + ColumnDef::new(Notes::OriginalDigestId) + .big_unsigned() + .null(), + ) + .col( + ColumnDef::new(Notes::Visibility) + .integer() + .not_null() + .default(0), + ) + .col( + ColumnDef::new(Notes::NonMemberViewsCount) + .integer() + .not_null() + .default(0), + ) + .col( + ColumnDef::new(Notes::ResolvedCommentsCount) + .integer() + .default(0_i32), + ) + .col(ColumnDef::new(Notes::LastActivityAt).date_time().null()) + .col(ColumnDef::new(Notes::ContentUpdatedAt).date_time().null()) + .to_owned(), + ) + .await?; + + manager + .create_index( + Index::create() + .if_not_exists() + .unique() + .name("index_notes_on_public_id") + .table(Notes::Table) + .col(Notes::PublicId) + .to_owned(), + ) + .await?; + manager + .create_index( + Index::create() + .if_not_exists() + .name("index_notes_on_content_updated_at") + .table(Notes::Table) + .col(Notes::ContentUpdatedAt) + .to_owned(), + ) + .await?; + manager + .create_index( + Index::create() + .if_not_exists() + .name("index_notes_on_created_at") + .table(Notes::Table) + .col(Notes::CreatedAt) + .to_owned(), + ) + .await?; + manager + .create_index( + Index::create() + .if_not_exists() + .name("index_notes_on_discarded_at") + .table(Notes::Table) + .col(Notes::DiscardedAt) + .to_owned(), + ) + .await?; + manager + .create_index( + Index::create() + .if_not_exists() + .name("index_notes_on_last_activity_at") + .table(Notes::Table) + .col(Notes::LastActivityAt) + .to_owned(), + ) + .await?; + manager + .create_index( + Index::create() + .if_not_exists() + .name("index_notes_on_user_id") + .table(Notes::Table) + .col(Notes::UserId) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .table(NoteViews::Table) + .if_not_exists() + .col(pk_bigint(NoteViews::Id)) + .col(ColumnDef::new(NoteViews::NoteId).big_unsigned().not_null()) + .col(ColumnDef::new(NoteViews::UserId).big_unsigned().not_null()) + .col(ColumnDef::new(NoteViews::CreatedAt).date_time().not_null()) + .col(ColumnDef::new(NoteViews::UpdatedAt).date_time().not_null()) + .to_owned(), + ) + .await?; + + manager + .create_index( + Index::create() + .if_not_exists() + .unique() + .name("index_note_views_on_note_id_and_user_id") + .table(NoteViews::Table) + .col(NoteViews::NoteId) + .col(NoteViews::UserId) + .to_owned(), + ) + .await?; + manager + .create_index( + Index::create() + .if_not_exists() + .name("index_note_views_on_note_id") + .table(NoteViews::Table) + .col(NoteViews::NoteId) + .to_owned(), + ) + .await?; + manager + .create_index( + Index::create() + .if_not_exists() + .name("index_note_views_on_user_id") + .table(NoteViews::Table) + .col(NoteViews::UserId) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .table(NonMemberNoteViews::Table) + .if_not_exists() + .col(pk_bigint(NonMemberNoteViews::Id)) + .col( + ColumnDef::new(NonMemberNoteViews::NoteId) + .big_unsigned() + .not_null(), + ) + .col( + ColumnDef::new(NonMemberNoteViews::UserId) + .big_unsigned() + .null(), + ) + .col( + ColumnDef::new(NonMemberNoteViews::AnonymizedIp) + .string_len(255) + .not_null(), + ) + .col(ColumnDef::new(NonMemberNoteViews::UserAgent).text()) + .col( + ColumnDef::new(NonMemberNoteViews::CreatedAt) + .date_time() + .not_null(), + ) + .col( + ColumnDef::new(NonMemberNoteViews::UpdatedAt) + .date_time() + .not_null(), + ) + .to_owned(), + ) + .await?; + + match manager.get_database_backend() { + DatabaseBackend::Postgres => { + manager + .create_index( + Index::create() + .if_not_exists() + .name("idx_non_member_note_views_on_note_ip_and_user_agent") + .table(NonMemberNoteViews::Table) + .col(NonMemberNoteViews::NoteId) + .col(NonMemberNoteViews::AnonymizedIp) + .col(NonMemberNoteViews::UserAgent) + .to_owned(), + ) + .await?; + } + _ => { + manager + .create_index( + Index::create() + .if_not_exists() + .name("idx_non_member_note_views_on_note_ip_and_user_agent") + .table(NonMemberNoteViews::Table) + .col(NonMemberNoteViews::NoteId) + .col(NonMemberNoteViews::AnonymizedIp) + .col((NonMemberNoteViews::UserAgent, 320_u32)) + .to_owned(), + ) + .await?; + } + } + manager + .create_index( + Index::create() + .if_not_exists() + .name("index_non_member_note_views_on_note_id_and_user_id") + .table(NonMemberNoteViews::Table) + .col(NonMemberNoteViews::NoteId) + .col(NonMemberNoteViews::UserId) + .to_owned(), + ) + .await?; + manager + .create_index( + Index::create() + .if_not_exists() + .name("index_non_member_note_views_on_note_id") + .table(NonMemberNoteViews::Table) + .col(NonMemberNoteViews::NoteId) + .to_owned(), + ) + .await?; + manager + .create_index( + Index::create() + .if_not_exists() + .name("index_non_member_note_views_on_user_id") + .table(NonMemberNoteViews::Table) + .col(NonMemberNoteViews::UserId) + .to_owned(), + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(NonMemberNoteViews::Table).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(NoteViews::Table).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(Notes::Table).to_owned()) + .await?; + + Ok(()) + } +} diff --git a/jupiter/src/migration/m20260324_033322_fix_migration.rs b/jupiter/src/migration/m20260324_033322_fix_migration.rs new file mode 100644 index 000000000..ac061b7a5 --- /dev/null +++ b/jupiter/src/migration/m20260324_033322_fix_migration.rs @@ -0,0 +1,88 @@ +use sea_orm::{DatabaseBackend, EnumIter, Iterable, sea_query::extension::postgres::Type}; +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + if let DatabaseBackend::Postgres = manager.get_database_backend() { + let conn = manager.get_connection(); + + manager + .create_type( + Type::create() + .as_enum(WebhookEventTypeEnum) + .values(WebhookEventTypeVariant::iter()) + .to_owned(), + ) + .await?; + + conn.execute_unprepared( + r#"ALTER TABLE mega_webhook_delivery + ALTER COLUMN event_type TYPE webhook_event_type_enum + USING event_type::webhook_event_type_enum"#, + ) + .await?; + + conn.execute_unprepared( + r#"ALTER TABLE mega_webhook_event_type + ALTER COLUMN event_type TYPE webhook_event_type_enum + USING event_type::webhook_event_type_enum"#, + ) + .await?; + } + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + if let DatabaseBackend::Postgres = manager.get_database_backend() { + let conn = manager.get_connection(); + + conn.execute_unprepared( + r#"ALTER TABLE mega_webhook_delivery + ALTER COLUMN event_type TYPE varchar USING event_type::text"#, + ) + .await?; + + conn.execute_unprepared( + r#"ALTER TABLE mega_webhook_event_type + ALTER COLUMN event_type TYPE varchar USING event_type::text"#, + ) + .await?; + + manager + .drop_type( + Type::drop() + .if_exists() + .name(WebhookEventTypeEnum) + .restrict() + .to_owned(), + ) + .await?; + } + Ok(()) + } +} + +#[derive(DeriveIden)] +struct WebhookEventTypeEnum; + +#[derive(Iden, EnumIter)] +enum WebhookEventTypeVariant { + #[iden = "cl.created"] + ClCreated, + #[iden = "cl.updated"] + ClUpdated, + #[iden = "cl.merged"] + ClMerged, + #[iden = "cl.closed"] + ClClosed, + #[iden = "cl.reopened"] + ClReopened, + #[iden = "cl.comment.created"] + ClCommentCreated, + #[iden = "all"] + All, +} diff --git a/jupiter/src/migration/mod.rs b/jupiter/src/migration/mod.rs index abe469f9b..a4b06764e 100644 --- a/jupiter/src/migration/mod.rs +++ b/jupiter/src/migration/mod.rs @@ -95,6 +95,8 @@ mod m20260308_191753_create_webhook; mod m20260308_220000_add_base_branch_to_mega_cl; mod m20260308_230000_normalize_webhook_event_types; mod m20260316_120000_add_bot_tokens_token_hash_index; +mod m20260324_024559_add_notes; +mod m20260324_033322_fix_migration; /// Creates a primary key column definition with big integer type. /// @@ -180,6 +182,8 @@ impl MigratorTrait for Migrator { Box::new(m20260308_220000_add_base_branch_to_mega_cl::Migration), Box::new(m20260308_230000_normalize_webhook_event_types::Migration), Box::new(m20260316_120000_add_bot_tokens_token_hash_index::Migration), + Box::new(m20260324_024559_add_notes::Migration), + Box::new(m20260324_033322_fix_migration::Migration), ] } } diff --git a/jupiter/src/service/webhook_service.rs b/jupiter/src/service/webhook_service.rs index ec507672b..dd4a2e9c5 100644 --- a/jupiter/src/service/webhook_service.rs +++ b/jupiter/src/service/webhook_service.rs @@ -128,7 +128,7 @@ impl WebhookService { let event_type_str = event_type.to_value(); let webhooks = self .storage - .find_matching_webhooks(event_type, path) + .find_matching_webhooks(event_type.clone(), path) .await?; for webhook in webhooks { @@ -168,7 +168,7 @@ impl WebhookService { let delivery = callisto::mega_webhook_delivery::Model { id: IdInstance::next_id(), webhook_id: webhook.id, - event_type, + event_type: event_type.clone(), payload: payload_json.clone(), response_status: Some(status as i32), response_body: Some(body), @@ -193,7 +193,7 @@ impl WebhookService { let delivery = callisto::mega_webhook_delivery::Model { id: IdInstance::next_id(), webhook_id: webhook.id, - event_type, + event_type: event_type.clone(), payload: payload_json.clone(), response_status: None, response_body: None, diff --git a/jupiter/src/storage/note_storage.rs b/jupiter/src/storage/note_storage.rs index a0c255496..db3d750c4 100644 --- a/jupiter/src/storage/note_storage.rs +++ b/jupiter/src/storage/note_storage.rs @@ -1,6 +1,7 @@ use std::ops::Deref; use callisto::notes; +use chrono::Utc; use common::errors::MegaError; use sea_orm::{ ActiveModelTrait, ActiveValue::Set, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, @@ -47,14 +48,16 @@ impl NoteStorage { &self, payload: CreateNotePayload, ) -> Result, MegaError> { + let now = Utc::now().naive_utc(); let note_active_model = notes::ActiveModel { public_id: Set(payload.public_id), - organization_membership_id: Set(payload.organization_membership_id), + user_id: Set(payload.user_id), title: Set(payload.title), description_html: Set(payload.description_html), description_state: Set(payload.description_state), - project_id: Set(payload.project_id), visibility: Set(payload.visibility.unwrap_or(0)), + created_at: Set(now), + updated_at: Set(now), ..Default::default() }; @@ -88,12 +91,11 @@ impl NoteStorage { #[derive(Clone, Debug)] pub struct CreateNotePayload { pub public_id: String, - pub organization_membership_id: i64, + pub user_id: i64, pub title: Option, pub description_html: Option, pub description_state: Option, - pub project_id: Option, pub visibility: Option, } diff --git a/jupiter/src/storage/webhook_storage.rs b/jupiter/src/storage/webhook_storage.rs index 3fcdb86c5..dd9688636 100644 --- a/jupiter/src/storage/webhook_storage.rs +++ b/jupiter/src/storage/webhook_storage.rs @@ -107,7 +107,7 @@ impl WebhookStorage { let candidates = mega_webhook::Entity::find() .join( JoinType::InnerJoin, - mega_webhook::Relation::WebhookEventTypes.def(), + mega_webhook::Relation::MegaWebhookEventType.def(), ) .filter(mega_webhook::Column::Active.eq(true)) .filter( @@ -156,7 +156,7 @@ impl WebhookStorage { for event_type in event_types { mega_webhook_event_type::ActiveModel { webhook_id: Set(webhook_id), - event_type: Set(*event_type), + event_type: Set(event_type.clone()), } .insert(self.get_connection()) .await?; @@ -190,10 +190,9 @@ impl WebhookStorage { } fn normalize_event_types(event_types: Vec) -> Vec { - let mut dedup = std::collections::HashSet::new(); let mut normalized = Vec::new(); for event in event_types { - if dedup.insert(event) { + if !normalized.contains(&event) { normalized.push(event); } } diff --git a/mono/src/api/oauth/mod.rs b/mono/src/api/oauth/mod.rs index 9099a9dda..c07cd0ead 100644 --- a/mono/src/api/oauth/mod.rs +++ b/mono/src/api/oauth/mod.rs @@ -1,3 +1,10 @@ +//! OAuth / session extractors for Axum API routes. +//! +//! Git smart HTTP (`/git-receive-pack`, etc.) is handled by [`crate::server::http_server::handle_smart_protocol`], +//! which takes a raw [`axum::http::Request`] and does not run `FromRequestParts`. For the same Mono access-token +//! validation as [`AccessTokenUser`], call [`bearer_token_from_authorization_value`] and +//! [`login_user_from_mono_access_token`] from that code path instead of the extractor. + use axum::{ RequestPartsExt, extract::{FromRef, FromRequestParts}, @@ -9,27 +16,13 @@ use axum_extra::{ headers::{self, Authorization, authorization::Bearer}, }; use callisto::{bot_tokens, bots}; +use common::errors::MegaError; use http::request::Parts; use jupiter::storage::user_storage::UserStorage; use model::LoginUser; use crate::api::{MonoApiServiceState, oauth::api_store::OAuthApiStore}; -/// Resolves `LoginUser` from a Mono-stored personal access token (e.g. Git HTTP `Authorization: Bearer`). -/// This path is independent of Campsite/Tinyship cookie session (`OAuthApiStore`). -async fn login_user_from_mono_access_token( - user_storage: &UserStorage, - token: &str, -) -> anyhow::Result> { - if let Some(username) = user_storage.find_user_by_token(token).await? { - return Ok(Some(LoginUser { - username, - ..Default::default() - })); - } - Ok(None) -} - pub mod api_store; pub mod campsite_store; pub mod model; @@ -48,6 +41,39 @@ pub struct BotIdentity { pub token: bot_tokens::Model, } +pub struct AccessTokenUser(pub LoginUser); + +/// Authenticated user resolved from a **browser session cookie** (Campsite or Tinyship), +/// not from `Authorization: Bearer` or the Mono DB access-token table. +/// +/// The Axum extractor reads the HTTP `Cookie` header, takes the value named by +/// [`OAuthApiStore::session_cookie_name`], and loads the user via [`OAuthApiStore::load_user_from_api`]. +/// For API clients that send a Mono access token in `Authorization`, use [`AccessTokenUser`] instead. +pub struct SessionUser(pub LoginUser); + +/// Parses a raw `Authorization` header value for `Bearer ` (case-insensitive `bearer` prefix). +/// Matches the Git HTTP receive-pack path so CLI clients and API routes share one rule. +pub fn bearer_token_from_authorization_value(value: &str) -> Option<&str> { + value + .strip_prefix("Bearer ") + .or_else(|| value.strip_prefix("bearer ")) + .map(str::trim) +} + +/// Validates a Mono DB access token; same as [`AccessTokenUser`] but usable outside Axum extractors. +pub async fn login_user_from_mono_access_token( + user_storage: &UserStorage, + token: &str, +) -> Result, MegaError> { + let Some(username) = user_storage.find_user_by_token(token).await? else { + return Ok(None); + }; + Ok(Some(LoginUser { + username, + ..Default::default() + })) +} + impl FromRequestParts for BotIdentity where MonoApiServiceState: FromRef, @@ -96,43 +122,57 @@ where } } -impl FromRequestParts for LoginUser +impl FromRequestParts for AccessTokenUser where - OAuthApiStore: FromRef, UserStorage: FromRef, S: Send + Sync, { - // If anything goes wrong or no session is found, redirect to the auth page type Rejection = AuthRedirect; async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { - let store = OAuthApiStore::from_ref(state); let user_storage = UserStorage::from_ref(state); - // Bearer: Mono personal access token (Git HTTP / CLI), not external session cookie. - if let Ok(TypedHeader(Authorization(bearer))) = - parts.extract::>>().await - { - let token = bearer.token(); - match login_user_from_mono_access_token(&user_storage, token).await { - Ok(Some(user)) => return Ok(user), - Ok(None) => { - tracing::debug!("LoginUser: invalid or expired bearer token"); - return Err(AuthRedirect); - } - Err(e) => { - tracing::warn!("LoginUser: error validating bearer token: {e:?}"); - return Err(AuthRedirect); - } + let TypedHeader(Authorization(bearer)) = parts + .extract::>>() + .await + .map_err(|e| { + tracing::debug!("AccessTokenUser: missing or invalid bearer token: {e}"); + AuthRedirect + })?; + + match login_user_from_mono_access_token(&user_storage, bearer.token()).await { + Ok(Some(user)) => Ok(AccessTokenUser(user)), + Ok(None) => { + tracing::debug!("AccessTokenUser: invalid or expired bearer token"); + Err(AuthRedirect) + } + Err(e) => { + tracing::warn!("AccessTokenUser: error validating bearer token: {e:?}"); + Err(AuthRedirect) } } + } +} + +impl FromRequestParts for SessionUser +where + OAuthApiStore: FromRef, + S: Send + Sync, +{ + type Rejection = AuthRedirect; + + /// Reads the session cookie from the request and resolves [`LoginUser`] through the + /// configured [`OAuthApiStore`] (external auth API). Missing cookie, unknown session, or + /// API errors become [`AuthRedirect`] (401). + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + let store = OAuthApiStore::from_ref(state); // Cookie: external auth session (Campsite or Tinyship per `OAuthApiStore`). let cookies = parts .extract::>() .await .map_err(|e| { - tracing::debug!("LoginUser: failed to read Cookie header: {e}"); + tracing::debug!("SessionUser: failed to read Cookie header: {e}"); AuthRedirect })?; @@ -142,15 +182,30 @@ where // Load user from external API match store.load_user_from_api(session_cookie.to_string()).await { - Ok(Some(user)) => Ok(user), + Ok(Some(user)) => Ok(SessionUser(user)), Ok(None) => { - tracing::debug!("LoginUser: invalid or expired session (external auth)"); + tracing::debug!("SessionUser: invalid or expired session (external auth)"); Err(AuthRedirect) } Err(e) => { - tracing::warn!("LoginUser: error loading user from cookie session: {e:?}"); + tracing::warn!("SessionUser: error loading user from cookie session: {e:?}"); Err(AuthRedirect) } } } } + +// Backward-compatible extractor: `LoginUser` now maps to cookie session only. +// Use `AccessTokenUser` explicitly where bearer token auth is required. +impl FromRequestParts for LoginUser +where + OAuthApiStore: FromRef, + S: Send + Sync, +{ + type Rejection = AuthRedirect; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + let SessionUser(user) = SessionUser::from_request_parts(parts, state).await?; + Ok(user) + } +} diff --git a/mono/src/git_protocol/http.rs b/mono/src/git_protocol/http.rs index ce76c5d2b..e5e3cdbf4 100644 --- a/mono/src/git_protocol/http.rs +++ b/mono/src/git_protocol/http.rs @@ -8,14 +8,18 @@ use axum::{ use bytes::{Bytes, BytesMut}; use ceres::{ api_service::state::ProtocolApiState, - protocol::{ServiceType, SmartProtocol, smart}, + protocol::{PushUserInfo, ServiceType, SmartProtocol, smart}, }; use common::errors::ProtocolError; use futures::{TryStreamExt, stream}; +use http::header::AUTHORIZATION; use tokio::io::AsyncReadExt; use tokio_stream::StreamExt; -use crate::git_protocol::InfoRefsParams; +use crate::{ + api::oauth::{bearer_token_from_authorization_value, login_user_from_mono_access_token}, + git_protocol::InfoRefsParams, +}; // # Discovering Reference // HTTP clients that support the "smart" protocol (or both the "smart" and "dumb" protocols) MUST @@ -48,13 +52,39 @@ fn auth_failed() -> Result, ProtocolError> { .status(401) .header( http::header::WWW_AUTHENTICATE, - HeaderValue::from_static("Basic realm=Mega"), + HeaderValue::from_static("Bearer realm=\"Mega\""), ) .body(Body::empty()) .unwrap(); Ok(resp) } +/// Uses [`crate::api::oauth::login_user_from_mono_access_token`] (same as [`crate::api::oauth::AccessTokenUser`]). +async fn git_receive_pack_bearer_auth( + state: &ProtocolApiState, + pack_protocol: &mut SmartProtocol, + headers: &http::HeaderMap, +) -> Result { + let token = headers + .get(AUTHORIZATION) + .and_then(|v| v.to_str().ok()) + .and_then(bearer_token_from_authorization_value); + let Some(token) = token else { + return Ok(false); + }; + + let Some(user) = + login_user_from_mono_access_token(&state.storage.user_storage(), token).await? + else { + return Ok(false); + }; + + let username = user.username; + pack_protocol.username = Some(username.clone()); + pack_protocol.authenticated_user = Some(PushUserInfo { username }); + Ok(true) +} + /// # Handles a Git upload pack request and prepares the response. /// /// The function takes a `req` parameter representing the HTTP request received and a `pack_protocol` @@ -145,8 +175,7 @@ pub async fn git_receive_pack( req: Request, mut pack_protocol: SmartProtocol, ) -> Result, ProtocolError> { - if pack_protocol.enable_http_auth(state) && !pack_protocol.http_auth(state, req.headers()).await - { + if !git_receive_pack_bearer_auth(state, &mut pack_protocol, req.headers()).await? { return auth_failed(); } // Convert the request body into a data stream. diff --git a/mono/tests/common/mod.rs b/mono/tests/common/mod.rs index 000168d38..7da132fe8 100644 --- a/mono/tests/common/mod.rs +++ b/mono/tests/common/mod.rs @@ -409,12 +409,6 @@ acquire_timeout = 3 connect_timeout = 3 sqlx_logging = false -[authentication] -enable_http_auth = true -enable_test_user = false -test_user_name = "mega" -test_user_token = "mega" - [monorepo] import_dir = "/tmp/mega/import" admin = ["admin"] diff --git a/orion-server/src/api.rs b/orion-server/src/api.rs index 3eae6d886..801dc50da 100644 --- a/orion-server/src/api.rs +++ b/orion-server/src/api.rs @@ -59,6 +59,7 @@ use crate::{ scheduler::{ BuildEventPayload, BuildInfo, TaskQueueStats, TaskScheduler, WorkerInfo, WorkerStatus, }, + service::target_build_status_service::TargetBuildStatusService, }; const RETRY_COUNT_MAX: i32 = 3; @@ -2839,7 +2840,7 @@ impl TargetStatusCache { action: event.target.action.clone(), }; - let active_model = target_build_status::Model::new_active_model( + let active_model = TargetBuildStatusService::new_active_model( Uuid::new_v4(), // id is not auto-incremented task_id, event.target.configured_target_package, @@ -2895,8 +2896,7 @@ impl TargetStatusCache { continue; } - if let Err(e) = - target_build_status::Entity::upsert_batch(&conn, models).await + if let Err(e) = TargetBuildStatusService::upsert_batch(&conn, models).await { tracing::error!("Auto flush failed: {:?}", e); } @@ -2908,7 +2908,7 @@ impl TargetStatusCache { let models = self.flush_all().await; if !models.is_empty() { - let _ = target_build_status::Entity::upsert_batch(&conn, models).await; + let _ = TargetBuildStatusService::upsert_batch(&conn, models).await; } break; @@ -2973,8 +2973,7 @@ pub async fn targets_status_handler( Err(_) => return Err((StatusCode::BAD_REQUEST, "Invalid task_id".to_string())), }; - let targets = match target_build_status::Entity::fetch_by_task_id(&state.conn, task_uuid).await - { + let targets = match TargetBuildStatusService::fetch_by_task_id(&state.conn, task_uuid).await { Ok(list) => list, Err(e) => { tracing::error!( @@ -3036,11 +3035,7 @@ pub async fn single_target_status_handle( }; // 查询数据库 - let target = match target_build_status::Entity::find() - .filter(target_build_status::Column::Id.eq(target_uuid)) - .one(&state.conn) - .await - { + let target = match TargetBuildStatusService::find_by_id(&state.conn, target_uuid).await { Ok(Some(t)) => t, Ok(None) => return Err((StatusCode::NOT_FOUND, "Target not found".to_string())), Err(e) => { diff --git a/orion-server/src/lib.rs b/orion-server/src/lib.rs index 604176449..8dd15b4d8 100644 --- a/orion-server/src/lib.rs +++ b/orion-server/src/lib.rs @@ -6,3 +6,4 @@ pub mod model; pub mod orion_common; pub mod scheduler; pub mod server; +pub mod service; diff --git a/orion-server/src/main.rs b/orion-server/src/main.rs index 9311a7c51..658885cd7 100644 --- a/orion-server/src/main.rs +++ b/orion-server/src/main.rs @@ -6,6 +6,7 @@ mod model; mod orion_common; mod scheduler; mod server; +mod service; /// Orion Build Server /// A distributed build system that manages build tasks and worker nodes diff --git a/orion-server/src/scheduler.rs b/orion-server/src/scheduler.rs index 3d1b10f0a..4c80e2915 100644 --- a/orion-server/src/scheduler.rs +++ b/orion-server/src/scheduler.rs @@ -711,14 +711,14 @@ mod tests { dequeued1.event_payload.build_event_id, task1.event_payload.build_event_id ); - assert_eq!(dequeued1.event_payload.repo, "/test/repo"); + assert_eq!(dequeued1.event_payload.repo, "test/repo"); let dequeued2 = queue.dequeue().unwrap(); assert_eq!( dequeued2.event_payload.build_event_id, task2.event_payload.build_event_id ); - assert_eq!(dequeued2.event_payload.repo, "/test2/repo"); + assert_eq!(dequeued2.event_payload.repo, "test2/repo"); } /// Test queue capacity limit diff --git a/orion-server/src/service/mod.rs b/orion-server/src/service/mod.rs new file mode 100644 index 000000000..1e2ba0992 --- /dev/null +++ b/orion-server/src/service/mod.rs @@ -0,0 +1 @@ +pub mod target_build_status_service; diff --git a/orion-server/src/service/target_build_status_service.rs b/orion-server/src/service/target_build_status_service.rs new file mode 100644 index 000000000..289cb403c --- /dev/null +++ b/orion-server/src/service/target_build_status_service.rs @@ -0,0 +1,90 @@ +use callisto::{sea_orm_active_enums::OrionTargetStatusEnum, target_build_status}; +use chrono::Utc; +use sea_orm::{ + ActiveValue::Set, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, + sea_query::OnConflict, +}; +use uuid::Uuid; + +pub struct TargetBuildStatusService; + +impl TargetBuildStatusService { + #[allow(clippy::too_many_arguments)] + pub fn new_active_model( + id: Uuid, + task_id: Uuid, + target_package: String, + target_name: String, + target_configuration: String, + category: String, + identifier: String, + action: String, + status: OrionTargetStatusEnum, + ) -> target_build_status::ActiveModel { + let now = Utc::now().into(); + target_build_status::ActiveModel { + id: Set(id), + task_id: Set(task_id), + target_package: Set(target_package), + target_name: Set(target_name), + target_configuration: Set(target_configuration), + category: Set(category), + identifier: Set(identifier), + action: Set(action), + status: Set(status), + created_at: Set(now), + updated_at: Set(now), + } + } + + pub async fn upsert_batch( + conn: &DatabaseConnection, + models: Vec, + ) -> Result<(), sea_orm::DbErr> { + if models.is_empty() { + return Ok(()); + } + + target_build_status::Entity::insert_many(models) + .on_conflict( + OnConflict::columns([ + target_build_status::Column::TaskId, + target_build_status::Column::TargetPackage, + target_build_status::Column::TargetName, + target_build_status::Column::TargetConfiguration, + target_build_status::Column::Category, + target_build_status::Column::Identifier, + target_build_status::Column::Action, + ]) + .update_columns([ + target_build_status::Column::Status, + target_build_status::Column::UpdatedAt, + ]) + .to_owned(), + ) + .exec(conn) + .await?; + + Ok(()) + } + + pub async fn fetch_by_task_id( + conn: &DatabaseConnection, + task_id: Uuid, + ) -> Result, sea_orm::DbErr> { + target_build_status::Entity::find() + .filter(target_build_status::Column::TaskId.eq(task_id)) + .all(conn) + .await + } + + pub async fn find_by_id( + conn: &DatabaseConnection, + id: Uuid, + ) -> Result, sea_orm::DbErr> { + target_build_status::Entity::find() + .filter(target_build_status::Column::Id.eq(id)) + .one(conn) + .await + } +} From b9cd3f57066297d409bdd8f428b88d548545d2e6 Mon Sep 17 00:00:00 2001 From: "benjamin.747" Date: Tue, 24 Mar 2026 16:17:56 +0800 Subject: [PATCH 2/2] fix: merge clippy --- .github/workflows/base.yml | 48 +++++++++++++++++++++++++++++++++++ orion-server/src/api.rs | 15 +++++------ orion-server/src/scheduler.rs | 1 + 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 95527c10a..53032c57c 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -53,6 +53,8 @@ jobs: env: CARGO_TERM_COLOR: always RUSTUP_TOOLCHAIN: stable + RUSTC_WRAPPER: sccache + SCCACHE_DIR: ${{ github.workspace }}/.sccache steps: - name: Checkout repository uses: actions/checkout@v4 @@ -64,9 +66,31 @@ jobs: with: cache-key: sysdeps platform: ubuntu + - name: Install sccache + run: | + sudo apt-get update + sudo apt-get install -y sccache + - name: Prepare sccache directory + run: | + mkdir -p "$SCCACHE_DIR" + - name: Cache sccache + uses: actions/cache@v4 + with: + path: ${{ env.SCCACHE_DIR }} + key: sccache-${{ runner.os }}-stable-${{ hashFiles('**/Cargo.lock') }}-clippy + restore-keys: | + sccache-${{ runner.os }}-stable-${{ hashFiles('**/Cargo.lock') }}- + sccache-${{ runner.os }}-stable- + - name: Rust cache + uses: Swatinem/rust-cache@v2 + with: + shared-key: base-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }} + cache-on-failure: true - name: Run cargo clippy run: | + sccache --start-server || true cargo +stable clippy --all-targets --all-features -- -D warnings + sccache --show-stats || true test: if: ${{ !(github.repository == 'web3infra-foundation/mega' && github.event_name == 'push') }} @@ -76,6 +100,8 @@ jobs: CARGO_TERM_COLOR: always RUSTUP_TOOLCHAIN: stable HOME: ${{ github.repository == 'web3infra-foundation/mega' && '/home/github' || '/home/runner' }} + RUSTC_WRAPPER: sccache + SCCACHE_DIR: ${{ github.workspace }}/.sccache steps: - name: Install Redis run: sudo apt-get update && sudo apt-get install -y redis-server @@ -93,6 +119,26 @@ jobs: platform: ubuntu self-hosted: ${{ github.repository == 'web3infra-foundation/mega' }} use-gtk: false + - name: Install sccache + run: | + sudo apt-get update + sudo apt-get install -y sccache + - name: Prepare sccache directory + run: | + mkdir -p "$SCCACHE_DIR" + - name: Cache sccache + uses: actions/cache@v4 + with: + path: ${{ env.SCCACHE_DIR }} + key: sccache-${{ runner.os }}-stable-${{ hashFiles('**/Cargo.lock') }}-test + restore-keys: | + sccache-${{ runner.os }}-stable-${{ hashFiles('**/Cargo.lock') }}- + sccache-${{ runner.os }}-stable- + - name: Rust cache + uses: Swatinem/rust-cache@v2 + with: + shared-key: base-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }} + cache-on-failure: true - name: Set up git lfs run: | @@ -104,12 +150,14 @@ jobs: - name: Run cargo test run: | + sccache --start-server || true cargo test --manifest-path common/Cargo.toml --all-features --no-fail-fast -- --nocapture cargo test --manifest-path jupiter/Cargo.toml --all-features --no-fail-fast -- --nocapture cargo test --manifest-path ceres/Cargo.toml --all-features --no-fail-fast -- --nocapture cargo test --manifest-path vault/Cargo.toml --all-features --no-fail-fast -- --nocapture cargo test --manifest-path saturn/Cargo.toml --all-features --no-fail-fast -- --nocapture cargo test --manifest-path orion-server/Cargo.toml --all-features --no-fail-fast -- --nocapture + sccache --show-stats || true # Note: The fuse/scorpio job has been removed as scorpio has been moved # to its own repository: https://github.com/web3infra-foundation/scorpiofs diff --git a/orion-server/src/api.rs b/orion-server/src/api.rs index 694bc6295..79d22d2b2 100644 --- a/orion-server/src/api.rs +++ b/orion-server/src/api.rs @@ -553,14 +553,14 @@ pub async fn task_handler_v2( ) .await; - return ( + ( StatusCode::OK, Json(OrionServerResponse { task_id: task_id.to_string(), results: vec![result], }), ) - .into_response(); + .into_response() } else { tracing::info!( "No idle workers available, attempting to enqueue task {}", @@ -578,25 +578,25 @@ pub async fn task_handler_v2( status: "queued".to_string(), message: "Task queued for processing when workers become available".to_string(), }; - return ( + ( StatusCode::OK, Json(OrionServerResponse { task_id: task_id.to_string(), results: vec![result], }), ) - .into_response(); + .into_response() } Err(e) => { tracing::warn!("Failed to queue task: {}", e); - return ( + ( StatusCode::SERVICE_UNAVAILABLE, Json(serde_json::json!({ "message": format!("Unable to queue task: {}", e) })), ) - .into_response(); + .into_response() } } } @@ -703,7 +703,6 @@ pub async fn task_handler( .into_response() } -#[allow(dead_code)] async fn handle_immediate_task_dispatch_v2( state: AppState, task_id: Uuid, @@ -2347,8 +2346,8 @@ pub struct BuildTargetDTO { } #[derive(ToSchema, Serialize)] -#[allow(dead_code)] pub enum BuildEventState { + #[allow(dead_code)] Pending, Running, Success, diff --git a/orion-server/src/scheduler.rs b/orion-server/src/scheduler.rs index c0148bb63..1381df284 100644 --- a/orion-server/src/scheduler.rs +++ b/orion-server/src/scheduler.rs @@ -175,6 +175,7 @@ pub struct PendingBuildEvent { } #[derive(Debug, Clone)] +#[allow(dead_code)] pub struct PendingBuildEventV2 { pub event_payload: BuildEventPayload, pub targets: Vec,