diff --git a/Cargo.lock b/Cargo.lock index c81a9d81317d..8d9c61cf46ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5541,6 +5541,7 @@ dependencies = [ "serde", "serde_json", "serial_test 3.0.0", + "strum", "tabby-common", "tabby-db", "tabby-scheduler", diff --git a/ee/tabby-db/migrations/0029_merged-provider-tables.up.sql b/ee/tabby-db/migrations/0029_merged-provider-tables.up.sql index f272586b97e7..8bbdeffe9200 100644 --- a/ee/tabby-db/migrations/0029_merged-provider-tables.up.sql +++ b/ee/tabby-db/migrations/0029_merged-provider-tables.up.sql @@ -11,6 +11,7 @@ CREATE TABLE integration_access_tokens( CREATE TABLE provided_repositories( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, integration_access_token_id INTEGER NOT NULL, + kind TEXT NOT NULL, vendor_id TEXT NOT NULL, name TEXT NOT NULL, git_url TEXT NOT NULL, diff --git a/ee/tabby-db/schema.sqlite b/ee/tabby-db/schema.sqlite index 4d5fbbb52d80..7031dd389929 100644 Binary files a/ee/tabby-db/schema.sqlite and b/ee/tabby-db/schema.sqlite differ diff --git a/ee/tabby-db/src/lib.rs b/ee/tabby-db/src/lib.rs index f76160b30633..e87d0dd20f2b 100644 --- a/ee/tabby-db/src/lib.rs +++ b/ee/tabby-db/src/lib.rs @@ -15,6 +15,7 @@ pub use gitlab_repository_provider::{GitlabProvidedRepositoryDAO, GitlabReposito pub use invitations::InvitationDAO; pub use job_runs::JobRunDAO; pub use oauth_credential::OAuthCredentialDAO; +pub use provided_repositories::ProvidedRepositoryDAO; pub use repositories::RepositoryDAO; pub use server_setting::ServerSettingDAO; use sqlx::{ diff --git a/ee/tabby-db/src/provided_repositories.rs b/ee/tabby-db/src/provided_repositories.rs index 4ed8630f1f9d..7c52a2469f6d 100644 --- a/ee/tabby-db/src/provided_repositories.rs +++ b/ee/tabby-db/src/provided_repositories.rs @@ -7,6 +7,7 @@ use crate::{DateTimeUtc, DbConn}; #[derive(FromRow)] pub struct ProvidedRepositoryDAO { pub id: i64, + pub kind: String, pub vendor_id: String, pub integration_access_token_id: i64, pub name: String, @@ -51,7 +52,7 @@ impl DbConn { pub async fn get_provided_repository(&self, id: i64) -> Result { let repo = query_as!( ProvidedRepositoryDAO, - "SELECT id, vendor_id, name, git_url, active, integration_access_token_id, created_at, updated_at FROM provided_repositories WHERE id = ?", + "SELECT id, vendor_id, kind, name, git_url, active, integration_access_token_id, created_at, updated_at FROM provided_repositories WHERE id = ?", id ) .fetch_one(&self.pool) @@ -95,6 +96,7 @@ impl DbConn { "vendor_id", "name", "git_url", + "kind", "active", "integration_access_token_id", "created_at" as "created_at: DateTimeUtc", diff --git a/ee/tabby-schema/src/dao.rs b/ee/tabby-schema/src/dao.rs index eaefdfe96154..db9623253ac2 100644 --- a/ee/tabby-schema/src/dao.rs +++ b/ee/tabby-schema/src/dao.rs @@ -4,20 +4,25 @@ use lazy_static::lazy_static; use tabby_db::{ EmailSettingDAO, GithubProvidedRepositoryDAO, GithubRepositoryProviderDAO, GitlabProvidedRepositoryDAO, GitlabRepositoryProviderDAO, InvitationDAO, JobRunDAO, - OAuthCredentialDAO, RepositoryDAO, ServerSettingDAO, UserDAO, UserEventDAO, + OAuthCredentialDAO, ProvidedRepositoryDAO, RepositoryDAO, ServerSettingDAO, UserDAO, + UserEventDAO, }; -use crate::schema::{ - auth::{self, OAuthCredential, OAuthProvider}, - email::{AuthMethod, EmailSetting, Encryption}, - job, - repository::{ - GitRepository, GithubProvidedRepository, GithubRepositoryProvider, - GitlabProvidedRepository, GitlabRepositoryProvider, RepositoryProviderStatus, +use crate::{ + integration::IntegrationKind, + repository::ProvidedRepository, + schema::{ + auth::{self, OAuthCredential, OAuthProvider}, + email::{AuthMethod, EmailSetting, Encryption}, + job, + repository::{ + GitRepository, GithubProvidedRepository, GithubRepositoryProvider, + GitlabProvidedRepository, GitlabRepositoryProvider, RepositoryProviderStatus, + }, + setting::{NetworkSetting, SecuritySetting}, + user_event::{EventKind, UserEvent}, + CoreError, }, - setting::{NetworkSetting, SecuritySetting}, - user_event::{EventKind, UserEvent}, - CoreError, }; impl From for auth::Invitation { @@ -124,6 +129,22 @@ impl From for NetworkSetting { } } +impl TryFrom for ProvidedRepository { + type Error = anyhow::Error; + fn try_from(value: ProvidedRepositoryDAO) -> Result { + Ok(Self { + integration_id: value.integration_access_token_id.as_id(), + active: value.active, + kind: IntegrationKind::from_enum_str(&value.kind)?, + display_name: value.name, + git_url: value.git_url, + vendor_id: value.vendor_id, + created_at: *value.created_at, + updated_at: *value.updated_at, + }) + } +} + impl From for GithubRepositoryProvider { fn from(value: GithubRepositoryProviderDAO) -> Self { Self { @@ -254,6 +275,23 @@ impl DbEnum for EventKind { } } +impl DbEnum for IntegrationKind { + fn as_enum_str(&self) -> &'static str { + match self { + IntegrationKind::Github => "github", + IntegrationKind::Gitlab => "gitlab", + } + } + + fn from_enum_str(s: &str) -> anyhow::Result { + match s { + "github" => Ok(IntegrationKind::Github), + "gitlab" => Ok(IntegrationKind::Gitlab), + _ => bail!("{s} is not a valid value for ProviderKind"), + } + } +} + impl DbEnum for Encryption { fn as_enum_str(&self) -> &'static str { match self { diff --git a/ee/tabby-schema/src/schema/integration.rs b/ee/tabby-schema/src/schema/integration.rs new file mode 100644 index 000000000000..715375075af7 --- /dev/null +++ b/ee/tabby-schema/src/schema/integration.rs @@ -0,0 +1,52 @@ +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use juniper::ID; +use strum::EnumIter; + +use crate::Result; + +#[derive(Clone, EnumIter)] +pub enum IntegrationKind { + Github, + Gitlab, +} + +pub enum IntegrationStatus { + Ready, + Pending, + Failed, +} + +pub struct IntegrationAccessToken { + pub id: ID, + pub kind: IntegrationKind, + pub display_name: String, + pub access_token: String, + pub created_at: DateTime, + pub updated_at: DateTime, + pub status: IntegrationStatus, +} + +#[async_trait] +pub trait IntegrationService: Send + Sync { + async fn create_integration( + &self, + kind: IntegrationKind, + display_name: String, + access_token: String, + ) -> Result; + + async fn delete_integration(&self, id: ID) -> Result<()>; + async fn update_integration(&self, id: ID, display_name: String, access_token: Option); + async fn list_integrations( + &self, + ids: Option>, + kind: Option, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result>; + + async fn sync_resources(&self, id: ID) -> Result<()>; +} diff --git a/ee/tabby-schema/src/schema/mod.rs b/ee/tabby-schema/src/schema/mod.rs index c86910fbc8dd..51a6a5edfe04 100644 --- a/ee/tabby-schema/src/schema/mod.rs +++ b/ee/tabby-schema/src/schema/mod.rs @@ -2,6 +2,7 @@ pub mod analytic; pub mod auth; pub mod constants; pub mod email; +pub mod integration; pub mod job; pub mod license; pub mod repository; diff --git a/ee/tabby-schema/src/schema/repository/mod.rs b/ee/tabby-schema/src/schema/repository/mod.rs index 3eee622a4480..cc33301cce43 100644 --- a/ee/tabby-schema/src/schema/repository/mod.rs +++ b/ee/tabby-schema/src/schema/repository/mod.rs @@ -10,6 +10,9 @@ pub use github::{GithubProvidedRepository, GithubRepositoryProvider, GithubRepos mod gitlab; use std::{path::PathBuf, sync::Arc}; +mod third_party; +pub use third_party::{ProvidedRepository, ThirdPartyRepositoryService}; + use async_trait::async_trait; pub use gitlab::{GitlabProvidedRepository, GitlabRepositoryProvider, GitlabRepositoryService}; use juniper::{GraphQLEnum, GraphQLObject, ID}; diff --git a/ee/tabby-schema/src/schema/repository/third_party.rs b/ee/tabby-schema/src/schema/repository/third_party.rs new file mode 100644 index 000000000000..5f6cbc39045f --- /dev/null +++ b/ee/tabby-schema/src/schema/repository/third_party.rs @@ -0,0 +1,32 @@ +use crate::{integration::IntegrationKind, schema::Result}; +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use juniper::ID; + +pub struct ProvidedRepository { + pub integration_id: ID, + pub active: bool, + pub kind: IntegrationKind, + pub display_name: String, + pub git_url: String, + pub vendor_id: String, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +#[async_trait] +pub trait ThirdPartyRepositoryService: Send + Sync { + async fn list_repositories( + &self, + integration_ids: Option>, + kind: Option, + active: Option, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result>; + + async fn update_repository_active(&self, id: ID, active: bool) -> Result<()>; + async fn list_active_git_urls(&self) -> Result>; +} diff --git a/ee/tabby-webserver/Cargo.toml b/ee/tabby-webserver/Cargo.toml index 938091abc781..507042fd082e 100644 --- a/ee/tabby-webserver/Cargo.toml +++ b/ee/tabby-webserver/Cargo.toml @@ -52,6 +52,7 @@ gitlab = "0.1610.0" apalis = { git = "https://github.com/wsxiaoys/apalis", rev = "91526e8", features = ["sqlite", "cron" ] } uuid.workspace = true hyper-util = { version = "0.1.3", features = ["client-legacy"] } +strum.workspace = true [dev-dependencies] assert_matches = "1.5.0" diff --git a/ee/tabby-webserver/src/service/repository/mod.rs b/ee/tabby-webserver/src/service/repository/mod.rs index cda3ffee4c0c..196d08bd72ed 100644 --- a/ee/tabby-webserver/src/service/repository/mod.rs +++ b/ee/tabby-webserver/src/service/repository/mod.rs @@ -1,6 +1,7 @@ mod git; mod github; mod gitlab; +mod third_party; use std::sync::Arc; diff --git a/ee/tabby-webserver/src/service/repository/third_party.rs b/ee/tabby-webserver/src/service/repository/third_party.rs new file mode 100644 index 000000000000..b4085a401e86 --- /dev/null +++ b/ee/tabby-webserver/src/service/repository/third_party.rs @@ -0,0 +1,78 @@ +use juniper::ID; +use std::marker::PhantomData; +use strum::IntoEnumIterator; +use tabby_schema::{ + integration::IntegrationKind, repository::ProvidedRepository, AsRowid, DbEnum, Result, +}; +use url::Url; + +use async_trait::async_trait; +use tabby_db::DbConn; +use tabby_schema::repository::{RepositoryProvider, ThirdPartyRepositoryService}; + +use crate::service::graphql_pagination_to_filter; + +struct ThirdPartyRepositoryServiceImpl { + db: DbConn, +} + +#[async_trait] +impl ThirdPartyRepositoryService for ThirdPartyRepositoryServiceImpl { + async fn list_repositories( + &self, + integration_ids: Option>, + kind: Option, + active: Option, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result> { + let (limit, skip_id, backwards) = graphql_pagination_to_filter(after, before, first, last)?; + + let integration_ids = integration_ids + .into_iter() + .flatten() + .map(|id| id.as_rowid()) + .collect::, _>>()?; + + let kind = kind.map(|kind| kind.as_enum_str().to_string()); + + Ok(self + .db + .list_provided_repositories(integration_ids, kind, active, limit, skip_id, backwards) + .await? + .into_iter() + .map(ProvidedRepository::try_from) + .collect::>()?) + } + + async fn update_repository_active(&self, id: ID, active: bool) -> Result<()> { + self.db + .update_provided_repository_active(id.as_rowid()?, active) + .await?; + Ok(()) + } + + async fn list_active_git_urls(&self) -> Result> { + let mut urls = vec![]; + } +} + +fn format_authenticated_url( + kind: &IntegrationKind, + git_url: &str, + access_token: &str, +) -> Result { + let mut url = Url::parse(git_url).map_err(anyhow::Error::from)?; + match kind { + IntegrationKind::Github => { + let _ = url.set_username(access_token); + } + IntegrationKind::Gitlab => { + let _ = url.set_username("oauth2"); + let _ = url.set_password(Some(access_token)); + } + } + Ok(url.to_string()) +}