Skip to content

Commit

Permalink
Merge branch 'main' into extract-prod-feature
Browse files Browse the repository at this point in the history
  • Loading branch information
wsxiaoys authored Apr 9, 2024
2 parents 562d55f + ed25f4a commit 03b3fc5
Show file tree
Hide file tree
Showing 18 changed files with 468 additions and 18 deletions.
1 change: 1 addition & 0 deletions ee/tabby-db/migrations/0022_github-provider.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE github_repository_provider;
8 changes: 8 additions & 0 deletions ee/tabby-db/migrations/0022_github-provider.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CREATE TABLE github_repository_provider(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
display_name TEXT NOT NULL,
application_id TEXT NOT NULL,
secret TEXT NOT NULL,
access_token TEXT,
CONSTRAINT `idx_application_id` UNIQUE (`application_id`)
);
Binary file modified ee/tabby-db/schema.sqlite
Binary file not shown.
98 changes: 98 additions & 0 deletions ee/tabby-db/src/github_repository_provider.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
use anyhow::{anyhow, Result};
use sqlx::{prelude::FromRow, query, query_as};
use tabby_db_macros::query_paged_as;

use crate::{DbConn, SQLXResultExt};

#[derive(FromRow)]
pub struct GithubRepositoryProviderDAO {
pub id: i64,
pub display_name: String,
pub application_id: String,
pub secret: String,
pub access_token: Option<String>,
}

impl DbConn {
pub async fn create_github_provider(
&self,
name: String,
application_id: String,
secret: String,
) -> Result<i64> {
let res = query!("INSERT INTO github_repository_provider (display_name, application_id, secret) VALUES ($1, $2, $3);",
name,
application_id,
secret
).execute(&self.pool).await.unique_error("GitHub Application ID already exists")?;
Ok(res.last_insert_rowid())
}

pub async fn get_github_provider(&self, id: i64) -> Result<GithubRepositoryProviderDAO> {
let provider = query_as!(
GithubRepositoryProviderDAO,
"SELECT id, display_name, application_id, secret, access_token FROM github_repository_provider WHERE id = ?;",
id
)
.fetch_one(&self.pool)
.await?;
Ok(provider)
}

pub async fn delete_github_provider(&self, id: i64) -> Result<()> {
let res = query!("DELETE FROM github_repository_provider WHERE id = ?;", id)
.execute(&self.pool)
.await?;
if res.rows_affected() != 1 {
return Err(anyhow!("No github provider details to delete"));
}
Ok(())
}

pub async fn update_github_provider_access_token(
&self,
id: i64,
access_token: String,
) -> Result<()> {
let res = query!(
"UPDATE github_repository_provider SET access_token = ? WHERE id = ?",
access_token,
id
)
.execute(&self.pool)
.await?;

if res.rows_affected() != 1 {
return Err(anyhow!(
"The specified Github repository provider does not exist"
));
}

Ok(())
}

pub async fn list_github_repository_providers(
&self,
limit: Option<usize>,
skip_id: Option<i32>,
backwards: bool,
) -> Result<Vec<GithubRepositoryProviderDAO>> {
let providers = query_paged_as!(
GithubRepositoryProviderDAO,
"github_repository_provider",
[
"id",
"display_name",
"application_id",
"secret",
"access_token"
],
limit,
skip_id,
backwards
)
.fetch_all(&self.pool)
.await?;
Ok(providers)
}
}
2 changes: 2 additions & 0 deletions ee/tabby-db/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use anyhow::anyhow;
use cache::Cache;
use chrono::{DateTime, NaiveDateTime, Utc};
pub use email_setting::EmailSettingDAO;
pub use github_repository_provider::GithubRepositoryProviderDAO;
pub use invitations::InvitationDAO;
pub use job_runs::JobRunDAO;
pub use oauth_credential::OAuthCredentialDAO;
Expand All @@ -17,6 +18,7 @@ pub use users::UserDAO;

pub mod cache;
mod email_setting;
mod github_repository_provider;
mod invitations;
mod job_runs;
mod oauth_credential;
Expand Down
17 changes: 17 additions & 0 deletions ee/tabby-webserver/graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ enum Language {
PYTHON
}

type GithubRepositoryProviderEdge {
node: GithubRepositoryProvider!
cursor: String!
}

type JobRun {
id: ID!
job: String!
Expand Down Expand Up @@ -44,6 +49,11 @@ type LicenseInfo {
expiresAt: DateTimeUtc
}

type GithubRepositoryProviderConnection {
edges: [GithubRepositoryProviderEdge!]!
pageInfo: PageInfo!
}

input SecuritySettingInput {
allowedRegisterDomainList: [String!]!
disableClientSideTelemetry: Boolean!
Expand Down Expand Up @@ -72,6 +82,12 @@ type ServerInfo {
allowSelfSignup: Boolean!
}

type GithubRepositoryProvider {
id: ID!
displayName: String!
applicationId: String!
}

input PasswordChangeInput {
oldPassword: String
newPassword1: String!
Expand Down Expand Up @@ -176,6 +192,7 @@ type Query {
me: User!
users(after: String, before: String, first: Int, last: Int): UserConnection!
invitations(after: String, before: String, first: Int, last: Int): InvitationConnection!
githubRepositoryProviders(after: String, before: String, first: Int, last: Int): GithubRepositoryProviderConnection!
jobRuns(ids: [ID!], jobs: [String!], after: String, before: String, first: Int, last: Int): JobRunConnection!
jobRunStats(jobs: [String!]): JobStats!
emailSetting: EmailSetting
Expand Down
5 changes: 5 additions & 0 deletions ee/tabby-webserver/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use tracing::{error, warn};
use crate::{
cron, hub, oauth,
path::db_file,
integrations
repositories::{self, RepositoryCache},

Check failure on line 25 in ee/tabby-webserver/src/handler.rs

View workflow job for this annotation

GitHub Actions / autofix

expected one of `,`, `::`, `as`, or `}`, found `repositories`
schema::{auth::AuthenticationService, create_schema, Schema, ServiceLocator},
service::{create_service_locator, event_logger::create_event_logger},
Expand Down Expand Up @@ -87,6 +88,10 @@ impl WebserverHandle {
"/repositories",
repositories::routes(rs.clone(), ctx.auth()),
)
.nest(
"/integrations/github",
integrations::github::routes(ctx.setting(), ctx.github_repository_provider()),
)
.route(
"/avatar/:id",
routing::get(avatar)
Expand Down
1 change: 1 addition & 0 deletions ee/tabby-webserver/src/integrations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod github;
152 changes: 152 additions & 0 deletions ee/tabby-webserver/src/integrations/github.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
use std::sync::Arc;

use anyhow::Result;
use axum::{
extract::{Path, Query, State},
response::Redirect,
routing, Router,
};
use hyper::StatusCode;
use juniper::ID;
use serde::Deserialize;
use tracing::error;
use url::Url;

use crate::{
oauth::github::GithubOAuthResponse,
schema::{
github_repository_provider::GithubRepositoryProviderService, setting::SettingService,
},
};

#[derive(Deserialize)]
struct CallbackParams {
state: ID,
code: String,
}

#[derive(Clone)]
struct IntegrationState {
pub settings: Arc<dyn SettingService>,
pub github_repository_provider: Arc<dyn GithubRepositoryProviderService>,
}

pub fn routes(
settings: Arc<dyn SettingService>,
github_repository_provider: Arc<dyn GithubRepositoryProviderService>,
) -> Router {
let state = IntegrationState {
settings,
github_repository_provider,
};
Router::new()
.route("/connect/:id", routing::get(connect))
.route("/callback", routing::get(callback))
.with_state(state)
}

fn get_authorize_url(client_id: &str, redirect_uri: &str, id: &ID) -> Result<Url> {
Ok(Url::parse_with_params(
"https://github.com/login/oauth/authorize",
&[
("client_id", client_id),
("response_type", "code"),
("scope", "repo"),
(
"redirect_uri",
&format!("{redirect_uri}/integrations/github/callback"),
),
("state", &id.to_string()),
],
)?)
}

async fn exchange_access_token(
state: &IntegrationState,
params: &CallbackParams,
) -> Result<GithubOAuthResponse> {
let client = reqwest::Client::new();

let client_id = state
.github_repository_provider
.get_github_repository_provider(params.state.clone())
.await?
.application_id;

let secret = state
.github_repository_provider
.read_github_repository_provider_secret(params.state.clone())
.await?;

Ok(client
.post("https://github.com/login/oauth/access_token")
.header(reqwest::header::ACCEPT, "application/json")
.form(&[
("client_id", &client_id),
("client_secret", &secret),
("code", &params.code),
])
.send()
.await?
.json()
.await?)
}

async fn callback(
State(state): State<IntegrationState>,
Query(params): Query<CallbackParams>,
) -> Result<Redirect, StatusCode> {
let response = match exchange_access_token(&state, &params).await {
Ok(response) => response,
Err(e) => {
error!("Failed to exchange access token: {e}");
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
};

if let Err(e) = state
.github_repository_provider
.update_github_repository_provider_access_token(params.state, response.access_token)
.await
{
error!("Failed to update github access token: {e}");
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}

Ok(Redirect::temporary("/"))
}

async fn connect(
State(state): State<IntegrationState>,
Path(id): Path<ID>,
) -> Result<Redirect, StatusCode> {
let network_setting = match state.settings.read_network_setting().await {
Ok(setting) => setting,
Err(e) => {
error!("Failed to read network setting: {e}");
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
};
let external_url = network_setting.external_url;
let provider = match state
.github_repository_provider
.get_github_repository_provider(id.clone())
.await
{
Ok(provider) => provider,
Err(e) => {
error!("Github repository provider not found: {e}");
return Err(StatusCode::NOT_FOUND);
}
};

let redirect = match get_authorize_url(&provider.application_id, &external_url, &id) {
Ok(redirect) => redirect,
Err(e) => {
error!("Failed to generate callback URL: {e}");
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
};

Ok(Redirect::temporary(redirect.as_str()))
}
1 change: 1 addition & 0 deletions ee/tabby-webserver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
mod cron;
mod handler;
mod hub;
mod integrations;
mod oauth;
mod path;
mod repositories;
Expand Down
14 changes: 7 additions & 7 deletions ee/tabby-webserver/src/oauth/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,20 @@ use crate::schema::auth::{AuthenticationService, OAuthCredential, OAuthProvider}

#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct GithubOAuthResponse {
pub struct GithubOAuthResponse {
#[serde(default)]
access_token: String,
pub access_token: String,
#[serde(default)]
scope: String,
pub scope: String,
#[serde(default)]
token_type: String,
pub token_type: String,

#[serde(default)]
error: String,
pub error: String,
#[serde(default)]
error_description: String,
pub error_description: String,
#[serde(default)]
error_uri: String,
pub error_uri: String,
}

#[derive(Debug, Deserialize)]
Expand Down
2 changes: 1 addition & 1 deletion ee/tabby-webserver/src/repositories/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,5 +87,5 @@ async fn meta(
}

async fn resolve(State(rs): State<Arc<ResolveState>>) -> Result<Response, StatusCode> {
rs.resolve_all().map_err(|_| StatusCode::NOT_FOUND)
rs.resolve_all().await.map_err(|_| StatusCode::NOT_FOUND)
}
Loading

0 comments on commit 03b3fc5

Please sign in to comment.