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 13 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",
integrations::routes(ctx.setting(), ctx.github_repository_provider()),
boxbeam marked this conversation as resolved.
Show resolved Hide resolved
)

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
34 changes: 34 additions & 0 deletions ee/tabby-webserver/src/integrations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use std::sync::Arc;

use axum::Router;
use juniper::ID;
use serde::Deserialize;

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

mod github;

#[derive(Clone)]
struct IntegrationsState {
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 = IntegrationsState {
settings,
github_repository_provider,
};
Router::new().nest("/github", github::routes(state))
}

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

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations.rs#L19-L28

Added lines #L19 - L28 were not covered by tests

#[derive(Deserialize)]

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

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations.rs#L30

Added line #L30 was not covered by tests
struct CallbackParams {
state: ID,
code: String,
}
119 changes: 119 additions & 0 deletions ee/tabby-webserver/src/integrations/github.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
use anyhow::Result;
use axum::{
extract::{Path, Query, State},
response::Redirect,
routing, Router,
};
use hyper::StatusCode;
use juniper::ID;
use tracing::error;

use crate::oauth::github::GithubOAuthResponse;

use super::{CallbackParams, IntegrationsState};

pub fn routes(state: IntegrationsState) -> Router {
Router::new()
.route("/connect/:id", routing::get(connect))
.route("/callback", routing::get(callback))
.with_state(state)
}

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

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L15-L20

Added lines #L15 - L20 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 24 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L22-L24

Added lines #L22 - L24 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: &IntegrationsState,
params: &CallbackParams,
) -> Result<GithubOAuthResponse> {
let client = reqwest::Client::new();

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

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L26-L30

Added lines #L26 - L30 were not covered by tests

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

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

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L32-L35

Added lines #L32 - L35 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 41 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L38-L41

Added lines #L38 - L41 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 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#L43-L55

Added lines #L43 - L55 were not covered by tests

async fn callback(
State(state): State<IntegrationsState>,
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 65 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-L65

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

Check warning on line 68 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

Added line #L68 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 74 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L70-L74

Added lines #L70 - L74 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 81 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L78-L81

Added lines #L78 - L81 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 88 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L83-L88

Added lines #L83 - L88 were not covered by tests

async fn connect(
State(state): State<IntegrationsState>,
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 98 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L90 - L98 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 105 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L101-L105

Added lines #L101 - L105 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 110 in ee/tabby-webserver/src/integrations/github.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L107-L110

Added lines #L107 - L110 were not covered by tests
}
};

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

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

View check run for this annotation

Codecov / codecov/patch

ee/tabby-webserver/src/integrations/github.rs#L114-L119

Added lines #L114 - L119 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