Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(db, webserver): Implement GitHubRepositoryProvider connect flow #1749

Merged
merged 18 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from 14 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
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`)
boxbeam marked this conversation as resolved.
Show resolved Hide resolved
);
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())
}

Check warning on line 29 in ee/tabby-db/src/github_repository_provider.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-db/src/github_repository_provider.rs#L17-L29

Added lines #L17 - L29 were not covered by tests

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)
}

Check warning on line 40 in ee/tabby-db/src/github_repository_provider.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-db/src/github_repository_provider.rs#L31-L40

Added lines #L31 - L40 were not covered by tests

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(())
}

Check warning on line 50 in ee/tabby-db/src/github_repository_provider.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-db/src/github_repository_provider.rs#L42-L50

Added lines #L42 - L50 were not covered by tests

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?;

Check warning on line 63 in ee/tabby-db/src/github_repository_provider.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-db/src/github_repository_provider.rs#L52-L63

Added lines #L52 - L63 were not covered by tests

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

Ok(())
}

Check warning on line 72 in ee/tabby-db/src/github_repository_provider.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-db/src/github_repository_provider.rs#L65-L72

Added lines #L65 - L72 were not covered by tests

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)
}

Check warning on line 97 in ee/tabby-db/src/github_repository_provider.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-db/src/github_repository_provider.rs#L74-L97

Added lines #L74 - L97 were not covered by tests
}
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
6 changes: 5 additions & 1 deletion ee/tabby-webserver/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
use tracing::{error, warn};

use crate::{
cron, hub, oauth,
cron, hub, integrations, oauth,
repositories::{self, RepositoryCache},
schema::{auth::AuthenticationService, create_schema, Schema, ServiceLocator},
service::{create_service_locator, event_logger::create_event_logger},
Expand Down Expand Up @@ -84,6 +84,10 @@
"/repositories",
repositories::routes(rs.clone(), ctx.auth()),
)
.nest(
"/integrations/github",
integrations::github::routes(ctx.setting(), ctx.github_repository_provider()),
)

Check warning on line 90 in ee/tabby-webserver/src/handler.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/handler.rs#L87-L90

Added lines #L87 - L90 were not covered by tests
.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;
144 changes: 144 additions & 0 deletions ee/tabby-webserver/src/integrations/github.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
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 crate::{
oauth::github::GithubOAuthResponse,
schema::{
github_repository_provider::GithubRepositoryProviderService, setting::SettingService,
},
};

#[derive(Deserialize)]

Check warning on line 21 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L21

Added line #L21 was not covered by tests
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)
}

Check warning on line 45 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L33-L45

Added lines #L33 - L45 were not covered by tests

fn github_redirect_url(client_id: &str, redirect_uri: &str, id: &ID) -> String {
boxbeam marked this conversation as resolved.
Show resolved Hide resolved
boxbeam marked this conversation as resolved.
Show resolved Hide resolved
format!("https://github.com/login/oauth/authorize?client_id={client_id}&response_type=code&scope=repo&redirect_uri={redirect_uri}/integrations/github/callback&state={id}")
}

Check warning on line 49 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L47-L49

Added lines #L47 - L49 were not covered by tests

async fn exchange_access_token(
Comment on lines +63 to +64
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
async fn exchange_access_token(
// FIXME(boxbeam): refactoring the logic using `/oauth/github.rs`
async fn exchange_access_token(

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

Check warning on line 55 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L51-L55

Added lines #L51 - L55 were not covered by tests

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

Check warning on line 60 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L57-L60

Added lines #L57 - L60 were not covered by tests
.application_id;

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

Check warning on line 66 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L63-L66

Added lines #L63 - L66 were not covered by tests

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?)
}

Check warning on line 80 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L68-L80

Added lines #L68 - L80 were not covered by tests

async fn callback(
State(state): State<IntegrationState>,
Query(params): Query<CallbackParams>,
) -> 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);

Check warning on line 90 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L82-L90

Added lines #L82 - L90 were not covered by tests
}
};
let external_url = network_setting.external_url;

Check warning on line 93 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L93

Added line #L93 was not covered by tests

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);

Check warning on line 99 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L95-L99

Added lines #L95 - L99 were not covered by tests
}
};

if let Err(e) = state
.github_repository_provider
.update_github_repository_provider_access_token(params.state, response.access_token)
.await

Check warning on line 106 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L103-L106

Added lines #L103 - L106 were not covered by tests
{
error!("Failed to update github access token: {e}");
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}

Ok(Redirect::permanent(&external_url))
boxbeam marked this conversation as resolved.
Show resolved Hide resolved
}

Check warning on line 113 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L108-L113

Added lines #L108 - L113 were not covered by tests

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);

Check warning on line 123 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L115-L123

Added lines #L115 - L123 were not covered by tests
}
};
let external_url = network_setting.external_url;
let provider = match state
.github_repository_provider
.get_github_repository_provider(id.clone())
.await

Check warning on line 130 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L126-L130

Added lines #L126 - L130 were not covered by tests
{
Ok(provider) => provider,
Err(e) => {
error!("Github repository provider not found: {e}");
return Err(StatusCode::NOT_FOUND);

Check warning on line 135 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L132-L135

Added lines #L132 - L135 were not covered by tests
}
};

Ok(Redirect::temporary(&github_redirect_url(
&provider.application_id,
&external_url,
&id,
)))
}

Check warning on line 144 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L139-L144

Added lines #L139 - L144 were not covered by tests
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 repositories;
mod schema;
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
49 changes: 49 additions & 0 deletions ee/tabby-webserver/src/schema/github_repository_provider.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use crate::schema::Result;
use async_trait::async_trait;
use juniper::{GraphQLObject, ID};
use juniper_axum::relay::NodeType;

use super::Context;

#[derive(GraphQLObject, Debug)]

Check warning on line 8 in ee/tabby-webserver/src/schema/github_repository_provider.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/schema/github_repository_provider.rs#L8

Added line #L8 was not covered by tests
#[graphql(context = Context)]
pub struct GithubRepositoryProvider {
pub id: ID,
pub display_name: String,
pub application_id: String,
}

impl NodeType for GithubRepositoryProvider {
type Cursor = String;

fn cursor(&self) -> Self::Cursor {
self.id.to_string()
}

Check warning on line 21 in ee/tabby-webserver/src/schema/github_repository_provider.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/schema/github_repository_provider.rs#L19-L21

Added lines #L19 - L21 were not covered by tests

fn connection_type_name() -> &'static str {
"GithubRepositoryProviderConnection"
}

Check warning on line 25 in ee/tabby-webserver/src/schema/github_repository_provider.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/schema/github_repository_provider.rs#L23-L25

Added lines #L23 - L25 were not covered by tests

fn edge_type_name() -> &'static str {
"GithubRepositoryProviderEdge"
}

Check warning on line 29 in ee/tabby-webserver/src/schema/github_repository_provider.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/schema/github_repository_provider.rs#L27-L29

Added lines #L27 - L29 were not covered by tests
}

#[async_trait]
pub trait GithubRepositoryProviderService: Send + Sync {
async fn get_github_repository_provider(&self, id: ID) -> Result<GithubRepositoryProvider>;
async fn read_github_repository_provider_secret(&self, id: ID) -> Result<String>;
async fn update_github_repository_provider_access_token(
&self,
id: ID,
access_token: String,
) -> Result<()>;

async fn list_github_repository_providers(
&self,
after: Option<String>,
before: Option<String>,
first: Option<usize>,
last: Option<usize>,
) -> Result<Vec<GithubRepositoryProvider>>;
}
Loading
Loading