From c352e3b109979260424b9c73cdbd387cf51c3401 Mon Sep 17 00:00:00 2001 From: "benjamin.747" Date: Fri, 26 Jul 2024 17:41:25 +0800 Subject: [PATCH 1/3] update policy and schema fields due to discussions --- saturn/entities.json | 24 +--------- saturn/mega.cedarschema | 19 ++------ saturn/mega_policies.cedar | 90 ++++++++++++-------------------------- saturn/src/main.rs | 12 ++++- saturn/src/objects.rs | 20 +-------- 5 files changed, 44 insertions(+), 121 deletions(-) diff --git a/saturn/entities.json b/saturn/entities.json index ff6f040b0..d30f39329 100644 --- a/saturn/entities.json +++ b/saturn/entities.json @@ -9,7 +9,7 @@ "User::\"alice\"": { "euid": "User::\"alice\"", "parents": [ - "UserGroup::\"mega_reader\"" + "UserGroup::\"mega_matainer\"" ] } }, @@ -19,9 +19,6 @@ "is_private": true, "admins": "UserGroup::\"mega_admin\"", "maintainers": "UserGroup::\"mega_matainer\"", - "writers": "UserGroup::\"mega_writer\"", - "triagers": "UserGroup::\"mega_triager\"", - "readers": "UserGroup::\"mega_reader\"", "parents": [] }, "Repository::\"public\"": { @@ -29,9 +26,6 @@ "is_private": false, "admins": "UserGroup::\"placeholder\"", "maintainers": "UserGroup::\"placeholder\"", - "writers": "UserGroup::\"placeholder\"", - "triagers": "UserGroup::\"placeholder\"", - "readers": "UserGroup::\"placeholder\"", "parents": [] } }, @@ -48,22 +42,6 @@ "UserGroup::\"mega_writer\"" ] }, - "UserGroup::\"mega_writer\"": { - "euid": "UserGroup::\"mega_writer\"", - "parents": [ - "UserGroup::\"mega_triager\"" - ] - }, - "UserGroup::\"mega_triager\"": { - "euid": "UserGroup::\"mega_triager\"", - "parents": [ - "UserGroup::\"mega_reader\"" - ] - }, - "UserGroup::\"mega_reader\"": { - "euid": "UserGroup::\"mega_reader\"", - "parents": [] - }, "UserGroup::\"placeholder\"": { "euid": "UserGroup::\"placeholder\"", "parents": [] diff --git a/saturn/mega.cedarschema b/saturn/mega.cedarschema index f3c5cd6a4..da6564b92 100644 --- a/saturn/mega.cedarschema +++ b/saturn/mega.cedarschema @@ -1,18 +1,10 @@ entity UserGroup in [UserGroup]; -entity User in [UserGroup, Team]; - -entity Team in [UserGroup] { - "admins": UserGroup, - "members": UserGroup, -}; +entity User in [UserGroup]; entity Repository = { "is_private": Bool, "admins": UserGroup, "maintainers": UserGroup, - "readers": UserGroup, - "triagers": UserGroup, - "writers": UserGroup, }; entity MergeRequest = { @@ -30,22 +22,17 @@ action "deleteRepo", "viewRepo", "forkRepo", "pullRepo", "pushRepo" appliesTo { resource: [Repository], }; -action "createMergeRequest", "closeMergeRequest", "approveMergeRequest" appliesTo { +action "createMergeRequest", "editMergeRequest", "deleteMergeRequest", "approveMergeRequest" appliesTo { principal: [User], resource: [MergeRequest], }; -action "createTeam", "viewTeam", "addMember", "removeMember", "deleteTeam" appliesTo { - principal: [User], - resource: [Team], -}; - action "openIssue", "assignIssue", "deleteIssue", "editIssue" appliesTo { principal: [User], resource: [Issue], }; -action "addReader", "addWriter", "addMaintainer", "addAdmin", "addTriager" appliesTo { +action "addMaintainer", "addAdmin" appliesTo { principal: [User], resource: [Repository], }; \ No newline at end of file diff --git a/saturn/mega_policies.cedar b/saturn/mega_policies.cedar index d8c549cfd..f21d1f12e 100644 --- a/saturn/mega_policies.cedar +++ b/saturn/mega_policies.cedar @@ -1,81 +1,33 @@ -// policy "anyoneCanViewPublicRepo" even if not login? +// policy "anyoneCanViewPublicRepo" permit ( principal, - action in [Action::"viewRepo"], + action in + [Action::"viewRepo", + Action::"pullRepo", + Action::"forkRepo", + Action::"pushRepo"], resource ) unless { resource.is_private }; -// policy "adminCanManageTeams" -// permit ( -// principal, -// action in -// [Action::"createTeam", -// Action::"deleteTeam", -// Action::"viewTeam", -// Action::"addMember", -// Action::"removeMember"], -// resource -// ) -// when { principal.role == "admin" }; - -// policy "memberCanViewTeam" -permit ( - principal, - action in [Action::"viewTeam"], - resource -) -when { principal in resource.members }; - -//Actions for readers -permit ( - principal, - action in [Action::"viewRepo", Action::"pullRepo", Action::"forkRepo"], - resource -) -when { principal in resource.readers }; - permit ( principal, action in [Action::"openIssue", Action::"createMergeRequest"], resource ) -when { principal in resource.repo.readers }; - -permit ( - principal, - action in [Action::"editIssue"], - resource -) -when { principal in resource.repo.readers && principal == resource.owner }; - -//Actions for triagers -permit ( - principal, - action == Action::"assignIssue", - resource -) -when { principal in resource.repo.triagers }; - -//Actions for writers -permit ( - principal, - action == Action::"pushRepo", - resource -) -when { principal in resource.writers }; +unless { resource.repo.is_private }; +//Actions for maintainers permit ( principal, - action in [Action::"editIssue", Action::"approveMergeRequest"], + action in [Action::"editIssue", Action::"editMergeRequest"], resource ) -when { principal in resource.repo.writers }; +when { principal in resource.repo.maintainers || principal == resource.owner }; -//Actions for maintainers permit ( principal, - action in [Action::"deleteIssue", Action::"approveMergeRequest"], + action in [Action::"assignIssue", Action::"approveMergeRequest"], resource ) when { principal in resource.repo.maintainers }; @@ -84,12 +36,24 @@ when { principal in resource.repo.maintainers }; permit ( principal, action in - [Action::"addReader", - Action::"addTriager", - Action::"addWriter", + [Action::"viewRepo", + Action::"pullRepo", + Action::"forkRepo", + Action::"pushRepo", Action::"addMaintainer", Action::"addAdmin", Action::"deleteRepo"], resource ) -when { principal in resource.admins }; \ No newline at end of file +when { principal in resource.admins }; + +permit ( + principal, + action in + [Action::"openIssue", + Action::"createMergeRequest", + Action::"deleteIssue", + Action::"deleteMergeRequest"], + resource +) +when { principal in resource.repo.admins }; \ No newline at end of file diff --git a/saturn/src/main.rs b/saturn/src/main.rs index e620e974a..42fe996e0 100644 --- a/saturn/src/main.rs +++ b/saturn/src/main.rs @@ -81,6 +81,7 @@ mod test { } fn load_context() -> AppContext { + tracing_subscriber::fmt().pretty().init(); AppContext::new( "./entities.json", "./mega.cedarschema", @@ -122,11 +123,20 @@ mod test { // anyone can view public_repo assert!(app_context .is_authorized( - anyone, + anyone.clone(), r#"Action::"viewRepo""#.parse::().unwrap(), resource.clone(), Context::empty() ) .is_ok()); + // anyone can not view mega + assert!(app_context + .is_authorized( + anyone, + r#"Action::"viewRepo""#.parse::().unwrap(), + r#"Repository::"mega""#.parse::().unwrap(), + Context::empty() + ) + .is_err()); } } diff --git a/saturn/src/objects.rs b/saturn/src/objects.rs index 9efa04cb5..fa932d880 100644 --- a/saturn/src/objects.rs +++ b/saturn/src/objects.rs @@ -21,7 +21,7 @@ impl From for Entity { } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct UserGroup { +pub struct UserGroup { euid: EntityUid, parents: HashSet, } @@ -41,9 +41,6 @@ pub struct Repo { is_private: bool, admins: EntityUid, maintainers: EntityUid, - readers: EntityUid, - triagers: EntityUid, - writers: EntityUid, parents: HashSet, } @@ -61,19 +58,7 @@ impl From for Entity { ( "maintainers", format!("{}", value.maintainers.as_ref()).parse().unwrap(), - ), - ( - "readers", - format!("{}", value.readers.as_ref()).parse().unwrap(), - ), - ( - "triagers", - format!("{}", value.triagers.as_ref()).parse().unwrap(), - ), - ( - "writers", - format!("{}", value.writers.as_ref()).parse().unwrap(), - ), + ) ] .into_iter() .map(|(x, v)| (x.into(), v)) @@ -81,7 +66,6 @@ impl From for Entity { let parents = value.parents.into_iter().map(|euid| euid.into()).collect(); - // let euid: EntityUid = value.euid.into(); Entity::new(value.euid.into(), attrs, parents).unwrap() } } From 3765a73c9670a2afb0af6249ee02fa4cba3841a3 Mon Sep 17 00:00:00 2001 From: "benjamin.747" Date: Sat, 27 Jul 2024 17:54:14 +0800 Subject: [PATCH 2/3] add new routers for handle oauth requests --- Cargo.toml | 1 + common/src/config.rs | 16 ++++ common/src/enums.rs | 20 ++++- gateway/Cargo.toml | 2 + gateway/src/api/mod.rs | 1 + gateway/src/api/oauth/github.rs | 72 ++++++++++++++++++ gateway/src/api/oauth/mod.rs | 126 ++++++++++++++++++++++++++++++++ gateway/src/api/oauth/model.rs | 28 +++++++ gateway/src/https_server.rs | 25 +++++-- mega/config.toml | 7 +- mercury/Cargo.toml | 2 +- 11 files changed, 292 insertions(+), 8 deletions(-) create mode 100644 gateway/src/api/oauth/github.rs create mode 100644 gateway/src/api/oauth/mod.rs create mode 100644 gateway/src/api/oauth/model.rs diff --git a/Cargo.toml b/Cargo.toml index 25ae08866..dbb465f05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,3 +70,4 @@ config = "0.14.0" shadow-rs = "0.30.0" reqwest = "0.12.5" lazy_static = "1.5.0" +uuid = "1.10.0" diff --git a/common/src/config.rs b/common/src/config.rs index fb1d92c1b..072b3e6df 100644 --- a/common/src/config.rs +++ b/common/src/config.rs @@ -19,6 +19,7 @@ pub struct Config { pub pack: PackConfig, pub ztm: ZTMConfig, pub lfs: LFSConfig, + pub oauth: OauthConfig, } impl Config { @@ -257,3 +258,18 @@ impl Default for LFSConfig { } } } + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct OauthConfig { + pub github_client_id: String, + pub github_client_secret: String, +} + +impl Default for OauthConfig { + fn default() -> Self { + Self { + github_client_id: String::new(), + github_client_secret: String::new(), + } + } +} \ No newline at end of file diff --git a/common/src/enums.rs b/common/src/enums.rs index 12f21a126..7e15bf2d9 100644 --- a/common/src/enums.rs +++ b/common/src/enums.rs @@ -7,6 +7,7 @@ //! consistency, especially when multiple modules need to work with the same //! set of enum variants. +use std::str::FromStr; use clap::ValueEnum; @@ -17,7 +18,7 @@ pub enum ZtmType { Relay, } -impl std::str::FromStr for ZtmType { +impl FromStr for ZtmType { type Err = String; fn from_str(s: &str) -> Result { @@ -28,3 +29,20 @@ impl std::str::FromStr for ZtmType { } } } + +/// An enum representing different oauth types. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub enum SupportOauthType { + GitHub, +} + +impl FromStr for SupportOauthType { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "github" => Ok(Self::GitHub), + _ => Err(format!("'{}' is not a valid oauth type", s)), + } + } +} diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index ce9d505ec..5d3471c79 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -38,5 +38,7 @@ tower-http = { workspace = true, features = [ "decompression-full", ] } tokio = { workspace = true, features = ["net"] } +reqwest = { workspace = true, features = ["json"] } +uuid = { workspace = true, features = ["v4"] } regex = "1.10.4" ed25519-dalek = { version = "2.1.1", features = ["pkcs8"] } diff --git a/gateway/src/api/mod.rs b/gateway/src/api/mod.rs index ce262680b..a47fa161e 100644 --- a/gateway/src/api/mod.rs +++ b/gateway/src/api/mod.rs @@ -8,6 +8,7 @@ use venus::import_repo::repo::Repo; pub mod api_router; pub mod mr_router; +pub mod oauth; #[derive(Clone)] pub struct ApiServiceState { diff --git a/gateway/src/api/oauth/github.rs b/gateway/src/api/oauth/github.rs new file mode 100644 index 000000000..bac27faab --- /dev/null +++ b/gateway/src/api/oauth/github.rs @@ -0,0 +1,72 @@ +use axum::async_trait; + +use common::errors::MegaError; +use jupiter::context::Context; + +use crate::api::oauth::model::{AuthorizeParams, GitHubAccessTokenJson, OauthCallbackParams}; +use crate::api::oauth::OauthHandler; + +use super::model::GitHubUserJson; + +#[derive(Clone)] +pub struct GithubOauthService { + pub context: Context, + pub client_id: String, + pub client_secret: String, +} + +const GITHUB_ENDPOINT: &str = "https://github.com"; +const GITHUB_API_ENDPOINT: &str = "https://api.github.com"; + +#[async_trait] +impl OauthHandler for GithubOauthService { + fn authorize_url(&self, params: &AuthorizeParams, state: &str) -> String { + let auth_url = format!( + "https://github.com/login/oauth/authorize?client_id={}&redirect_uri={}&state={}", + self.client_id, params.redirect_uri, state + ); + auth_url + } + + async fn access_token( + &self, + params: OauthCallbackParams, + redirect_uri: &str, + ) -> Result { + tracing::debug!("{:?}", params); + // get access_token and user for persist + let url = format!( + "{}/login/oauth/access_token?client_id={}&client_secret={}&code={}&redirect_uri={}", + GITHUB_ENDPOINT, self.client_id, self.client_secret, params.code, redirect_uri + ); + let client = reqwest::Client::new(); + let resp = client + .post(url) + .header("Accept", "application/json") + .send() + .await + .unwrap(); + let access_token = resp + .json::() + .await + .unwrap() + .access_token; + Ok(access_token) + } + + async fn user_info(&self, access_token: &str) -> Result { + let user_url = format!("{}/user", GITHUB_API_ENDPOINT); + let client = reqwest::Client::new(); + let resp = client + .get(user_url) + .header("Authorization", format!("Bearer {}", access_token)) + .header("Accept", "application/json") + .header("User-Agent", format!("Mega/{}", "0.0.1")) + .send() + .await + .unwrap(); + // tracing::debug!("user_resp: {:?}", resp.text().await.unwrap()); + let user_info = resp.json::().await.unwrap(); + Ok(user_info) + } +} diff --git a/gateway/src/api/oauth/mod.rs b/gateway/src/api/oauth/mod.rs new file mode 100644 index 000000000..e13e4ee2a --- /dev/null +++ b/gateway/src/api/oauth/mod.rs @@ -0,0 +1,126 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use axum::async_trait; +use axum::response::Redirect; +use axum::{ + extract::{Path, Query, State}, + http::StatusCode, + routing::get, + Json, Router, +}; +use tokio::sync::Mutex; +use uuid::Uuid; + +use common::enums::SupportOauthType; +use common::errors::MegaError; +use github::GithubOauthService; +use jupiter::context::Context; +use model::{AuthorizeParams, GitHubUserJson, OauthCallbackParams}; + +pub mod github; +pub mod model; + +#[derive(Clone)] +pub struct OauthServiceState { + pub context: Context, + pub sessions: Arc>>, +} + +impl OauthServiceState { + pub fn oauth_handler(&self, ouath_type: SupportOauthType) -> impl OauthHandler { + match ouath_type { + SupportOauthType::GitHub => GithubOauthService { + context: self.context.clone(), + client_id: self.context.config.oauth.github_client_id.clone(), + client_secret: self.context.config.oauth.github_client_secret.clone(), + }, + } + } +} + +#[async_trait] +pub trait OauthHandler: Send + Sync { + fn authorize_url(&self, params: &AuthorizeParams, state: &str) -> String; + + async fn access_token( + &self, + params: OauthCallbackParams, + redirect_uri: &str, + ) -> Result; + + async fn user_info(&self, access_token: &str) -> Result; +} + +pub fn routers() -> Router { + Router::new() + .route("/:oauth_type/authorize", get(redirect_authorize)) + .route("/:oauth_type/callback", get(oauth_callback)) + .route("/:oauth_type/user", get(user)) +} + +async fn redirect_authorize( + Path(oauth_type): Path, + Query(query): Query, + service_state: State, +) -> Result { + let oauth_type: SupportOauthType = match oauth_type.parse::() { + Ok(value) => value, + Err(err) => return Err((StatusCode::BAD_REQUEST, err)), + }; + + let mut sessions = service_state.sessions.lock().await; + let state = Uuid::new_v4().to_string(); + sessions.insert(state.clone(), query.redirect_uri.clone()); + let auth_url = service_state + .oauth_handler(oauth_type) + .authorize_url(&query, &state); + Ok(Redirect::temporary(&auth_url)) +} + +async fn oauth_callback( + Path(oauth_type): Path, + Query(query): Query, + service_state: State, +) -> Result { + let oauth_type: SupportOauthType = match oauth_type.parse::() { + Ok(value) => value, + Err(err) => return Err((StatusCode::BAD_REQUEST, err)), + }; + // chcek state, + // TODO storage can be replaced by redis, otherwise invalid state can't be expired + let mut sessions = service_state.sessions.lock().await; + + let redirect_uri = match sessions.get(&query.state) { + Some(uri) => uri.clone(), + None => return Err((StatusCode::BAD_REQUEST, "Invalid state".to_string())), + }; + let access_token = service_state + .oauth_handler(oauth_type) + .access_token(query.clone(), &redirect_uri) + .await + .unwrap(); + sessions.remove(&query.state); + + let callback_url = format!("{}/login?access_token={}", redirect_uri, access_token); + Ok(Redirect::temporary(&callback_url)) +} + +async fn user( + Path(oauth_type): Path, + Query(query): Query>, + service_state: State, +) -> Result, (StatusCode, String)> { + let oauth_type: SupportOauthType = match oauth_type.parse::() { + Ok(value) => value, + Err(err) => return Err((StatusCode::BAD_REQUEST, err)), + }; + let access_token = query.get("access_token").unwrap(); + + let res = service_state + .oauth_handler(oauth_type) + .user_info(access_token) + .await + .unwrap(); + Ok(Json(res)) +} diff --git a/gateway/src/api/oauth/model.rs b/gateway/src/api/oauth/model.rs new file mode 100644 index 000000000..8454b1a8c --- /dev/null +++ b/gateway/src/api/oauth/model.rs @@ -0,0 +1,28 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug)] +pub struct AuthorizeParams { + pub redirect_uri: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct OauthCallbackParams { + pub code: String, + pub state: String, +} + + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct GitHubAccessTokenJson { + pub access_token: String, + pub scope: Option, + pub token_type: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct GitHubUserJson { + pub login: String, + pub id: u32, + pub avatar_url: String, + pub email: String, +} diff --git a/gateway/src/https_server.rs b/gateway/src/https_server.rs index 4bf4c17af..c0928f358 100644 --- a/gateway/src/https_server.rs +++ b/gateway/src/https_server.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::net::SocketAddr; use std::ops::Deref; use std::path::PathBuf; @@ -14,10 +15,8 @@ use axum::routing::get; use axum::Router; use axum_server::tls_rustls::RustlsConfig; use clap::Args; -use common::enums::ZtmType; -use gemini::ztm::agent::{run_ztm_client, LocalZTMAgent}; -use gemini::ztm::hub::LocalZTMHub; use regex::Regex; +use tokio::sync::Mutex; use tower::ServiceBuilder; use tower_http::cors::{Any, CorsLayer}; use tower_http::decompression::RequestDecompressionLayer; @@ -26,11 +25,15 @@ use tower_http::trace::TraceLayer; use ceres::lfs::LfsConfig; use ceres::protocol::{SmartProtocol, TransportProtocol}; use common::config::Config; +use common::enums::ZtmType; use common::model::{CommonOptions, GetParams}; +use gemini::ztm::agent::{run_ztm_client, LocalZTMAgent}; +use gemini::ztm::hub::LocalZTMHub; use jupiter::context::Context; use jupiter::raw_storage::local_storage::LocalStorage; use crate::api::api_router::{self}; +use crate::api::oauth::{self, OauthServiceState}; use crate::api::ApiServiceState; use crate::ca_server::run_ca_server; use crate::lfs; @@ -140,13 +143,25 @@ pub async fn app(config: Config, host: String, port: u16, common: CommonOptions) common: common.clone(), }; - let api_state = ApiServiceState { context }; + let api_state = ApiServiceState { + context: context.clone(), + }; // add RequestDecompressionLayer for handle gzip encode // add TraceLayer for log record // add CorsLayer to add cors header Router::new() - .nest("/api/v1", api_router::routers().with_state(api_state)) + .nest( + "/api/v1", + api_router::routers().with_state(api_state.clone()), + ) + .nest( + "/auth", + oauth::routers().with_state(OauthServiceState { + context, + sessions: Arc::new(Mutex::new(HashMap::new())), + }), + ) .route( "/*path", get(get_method_router) diff --git a/mega/config.toml b/mega/config.toml index e8417e79d..8c1b6429f 100644 --- a/mega/config.toml +++ b/mega/config.toml @@ -91,4 +91,9 @@ agent = "http://127.0.0.1:7777" enable_split = true # Default is disabled. Set to true to enable file splitting. # Size of each file chunk when splitting is enabled, in bytes. Ignored if splitting is disabled. -split_size = 20971520 # Default size is 20MB (20971520 bytes) \ No newline at end of file +split_size = 20971520 # Default size is 20MB (20971520 bytes) + +[oauth] +# GitHub OAuth application client id and secret +github_client_id = "" +github_client_secret = "" \ No newline at end of file diff --git a/mercury/Cargo.toml b/mercury/Cargo.toml index 10463a33f..01eabf625 100644 --- a/mercury/Cargo.toml +++ b/mercury/Cargo.toml @@ -21,7 +21,7 @@ sha1 = { workspace = true } colored = { workspace = true } chrono = { workspace = true } tracing-subscriber = { workspace = true } -uuid = { version = "1.7.0", features = ["v4"] } +uuid = { workspace = true, features = ["v4"] } sha1_smol = "1.0.0" threadpool = "1.8.1" num_cpus.workspace = true From df1cbf8afe87eac60bed57a4b828db92ca74f540 Mon Sep 17 00:00:00 2001 From: "benjamin.747" Date: Mon, 29 Jul 2024 17:50:48 +0800 Subject: [PATCH 3/3] added oauth login page --- Cargo.toml | 1 + common/src/config.rs | 11 +-- gateway/Cargo.toml | 1 + gateway/src/api/oauth/mod.rs | 11 +-- gateway/src/https_server.rs | 9 ++- lunar/.env.local | 3 +- lunar/package.json | 7 +- lunar/src/app/api/fetcher.ts | 42 ++++++++++ lunar/src/app/api/relay/repo_fork/route.ts | 2 +- lunar/src/app/application-layout.tsx | 86 ++++++++++++++------- lunar/src/app/auth/github/callback/page.tsx | 21 +++++ lunar/src/app/login/layout.tsx | 18 +++++ lunar/src/app/login/page.tsx | 72 +++++++++++++++++ 13 files changed, 236 insertions(+), 48 deletions(-) create mode 100644 lunar/src/app/api/fetcher.ts create mode 100644 lunar/src/app/auth/github/callback/page.tsx create mode 100644 lunar/src/app/login/layout.tsx create mode 100644 lunar/src/app/login/page.tsx diff --git a/Cargo.toml b/Cargo.toml index dbb465f05..7ae589ac9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,7 @@ go-defer = "0.1.0" russh = "0.44.0" russh-keys = "0.44.0" axum = "0.7.5" +axum-extra = "0.9.3" tower-http = "0.5.2" tower = "0.4.13" hex = "0.4.3" diff --git a/common/src/config.rs b/common/src/config.rs index 072b3e6df..e1d6215cb 100644 --- a/common/src/config.rs +++ b/common/src/config.rs @@ -259,17 +259,8 @@ impl Default for LFSConfig { } } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, Default)] pub struct OauthConfig { pub github_client_id: String, pub github_client_secret: String, } - -impl Default for OauthConfig { - fn default() -> Self { - Self { - github_client_id: String::new(), - github_client_secret: String::new(), - } - } -} \ No newline at end of file diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index 5d3471c79..2ddb97e8d 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -37,6 +37,7 @@ tower-http = { workspace = true, features = [ "trace", "decompression-full", ] } +axum-extra = { workspace = true, features = ["typed-header"]} tokio = { workspace = true, features = ["net"] } reqwest = { workspace = true, features = ["json"] } uuid = { workspace = true, features = ["v4"] } diff --git a/gateway/src/api/oauth/mod.rs b/gateway/src/api/oauth/mod.rs index e13e4ee2a..50b720eaf 100644 --- a/gateway/src/api/oauth/mod.rs +++ b/gateway/src/api/oauth/mod.rs @@ -9,6 +9,9 @@ use axum::{ routing::get, Json, Router, }; +use axum_extra::headers::authorization::Bearer; +use axum_extra::headers::Authorization; +use axum_extra::TypedHeader; use tokio::sync::Mutex; use uuid::Uuid; @@ -102,24 +105,22 @@ async fn oauth_callback( .unwrap(); sessions.remove(&query.state); - let callback_url = format!("{}/login?access_token={}", redirect_uri, access_token); + let callback_url = format!("{}?access_token={}", redirect_uri, access_token); Ok(Redirect::temporary(&callback_url)) } async fn user( Path(oauth_type): Path, - Query(query): Query>, + TypedHeader(Authorization::(token)): TypedHeader>, service_state: State, ) -> Result, (StatusCode, String)> { let oauth_type: SupportOauthType = match oauth_type.parse::() { Ok(value) => value, Err(err) => return Err((StatusCode::BAD_REQUEST, err)), }; - let access_token = query.get("access_token").unwrap(); - let res = service_state .oauth_handler(oauth_type) - .user_info(access_token) + .user_info(token.token()) .await .unwrap(); Ok(Json(res)) diff --git a/gateway/src/https_server.rs b/gateway/src/https_server.rs index c0928f358..96a008151 100644 --- a/gateway/src/https_server.rs +++ b/gateway/src/https_server.rs @@ -9,7 +9,7 @@ use std::{thread, time}; use anyhow::Result; use axum::body::Body; use axum::extract::{Query, State}; -use axum::http::{Request, StatusCode, Uri}; +use axum::http::{self, Request, StatusCode, Uri}; use axum::response::Response; use axum::routing::get; use axum::Router; @@ -168,7 +168,12 @@ pub async fn app(config: Config, host: String, port: u16, common: CommonOptions) .post(post_method_router) .put(put_method_router), ) - .layer(ServiceBuilder::new().layer(CorsLayer::new().allow_origin(Any))) + .layer( + ServiceBuilder::new().layer(CorsLayer::new().allow_origin(Any).allow_headers(vec![ + http::header::AUTHORIZATION, + http::header::CONTENT_TYPE, + ])), + ) .layer(TraceLayer::new_for_http()) .layer(RequestDecompressionLayer::new()) .with_state(state) diff --git a/lunar/.env.local b/lunar/.env.local index 44ffeabea..3f0bbef9a 100644 --- a/lunar/.env.local +++ b/lunar/.env.local @@ -1,2 +1,3 @@ RELAY_API_URL=http://34.84.172.121 -MEGA_API_URL=http://localhost:8000 \ No newline at end of file +NEXT_PUBLIC_API_URL=http://localhost:8000 +NEXT_PUBLIC_CALLBACK_URL=http://localhost:3000/auth/github/callback \ No newline at end of file diff --git a/lunar/package.json b/lunar/package.json index 8e119f7cc..2d1159751 100644 --- a/lunar/package.json +++ b/lunar/package.json @@ -21,14 +21,15 @@ "framer-motion": "^11.3.2", "next": "14.2.3", "react": "^18", - "react-dom": "^18" + "react-dom": "^18", + "swr": "^2.2.5" }, "devDependencies": { - "@types/node": "^20", + "@types/node": "^22", "@types/react": "^18", "@types/react-dom": "^18", "autoprefixer": "^10.4.16", - "eslint": "^8", + "eslint": "^9.8", "eslint-config-next": "14.0.3", "postcss": "^8.4.31", "typescript": "^5" diff --git a/lunar/src/app/api/fetcher.ts b/lunar/src/app/api/fetcher.ts new file mode 100644 index 000000000..46712c390 --- /dev/null +++ b/lunar/src/app/api/fetcher.ts @@ -0,0 +1,42 @@ +import useSWR from "swr"; + +const endpoint = process.env.NEXT_PUBLIC_API_URL; + +const fetchWithToken = async (url, token) => { + + return fetch(url, { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + } + }).then(res => { + if (!res.ok) { + throw new Error('An error occurred while fetching the data.'); + } + return res.json(); + }); +}; + +// const fetcher = async url => { +// const res = await fetch(url) + +// if (!res.ok) { +// const error = new Error('An error occurred while fetching the data.') +// error.info = await res.json() +// error.status = res.status +// throw error +// } +// return res.json() +// } + +export function useUser(token) { + + const { data, error, isLoading } = useSWR(token ? [`${endpoint}/auth/github/user`, token] : null, ([url, token]) => fetchWithToken(url, token), { + dedupingInterval: 300000, // The request will not be repeated for 5 minutes + }) + return { + user: data, + isLoading, + isError: error, + } +} \ No newline at end of file diff --git a/lunar/src/app/api/relay/repo_fork/route.ts b/lunar/src/app/api/relay/repo_fork/route.ts index 83600a90e..4c23058b5 100644 --- a/lunar/src/app/api/relay/repo_fork/route.ts +++ b/lunar/src/app/api/relay/repo_fork/route.ts @@ -2,7 +2,7 @@ export const revalidate = 0 import { NextRequest } from "next/server"; -const endpoint = process.env.MEGA_API_URL; +const endpoint = process.env.NEXT_PUBLIC_API_URL; export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams diff --git a/lunar/src/app/application-layout.tsx b/lunar/src/app/application-layout.tsx index 12458c396..efabf9e3c 100644 --- a/lunar/src/app/application-layout.tsx +++ b/lunar/src/app/application-layout.tsx @@ -41,6 +41,10 @@ import { TicketIcon, } from '@heroicons/react/20/solid' import { usePathname } from 'next/navigation' +import { useUser } from '@/app/api/fetcher'; +import { Skeleton } from "antd"; +import { Button } from '@/components/catalyst/button' +import { useState, useEffect } from 'react' function AccountDropdownMenu({ anchor }: { anchor: 'top start' | 'bottom end' }) { return ( @@ -75,20 +79,38 @@ export function ApplicationLayout({ children: React.ReactNode }) { let pathname = usePathname() + const [token, setToken] = useState("") + useEffect(() => { + if (typeof window !== 'undefined') { + setToken(localStorage.getItem("access_token") || "") + } + }, []) + + const { user, isLoading, isError } = useUser(token); + if (isLoading) return ; return ( - - - - - - - - + { + !token && + + } + { + token && + + + + + + + + + + } + } sidebar={ @@ -127,15 +149,19 @@ export function ApplicationLayout({ - Home + Code & Issue - + - Issue + AI Chat - + - Merge Request + Reminder + + + + Logs @@ -156,22 +182,30 @@ export function ApplicationLayout({ + - - - - - - Admin - - Admin@mega.com + {token && + + + + + + {user.login} + + {user.email} + - - - - - + + + + + } + { + !token && + + } + } diff --git a/lunar/src/app/auth/github/callback/page.tsx b/lunar/src/app/auth/github/callback/page.tsx new file mode 100644 index 000000000..4018ef5c8 --- /dev/null +++ b/lunar/src/app/auth/github/callback/page.tsx @@ -0,0 +1,21 @@ +'use client' + +import { useSearchParams } from 'next/navigation'; +import { useRouter } from 'next/navigation'; + +export default function AuthPage() { + const router = useRouter(); + const apiUrl = process.env.NEXT_PUBLIC_API_URL; + const searchParams = useSearchParams(); + const access_token = searchParams.get('access_token') || ""; + const code = searchParams.get('code') || ""; + const state = searchParams.get('state') || ""; + + if (code && state) { + const targetUrl = `${apiUrl}/auth/github/callback?code=${code}&state=${state}`; + window.location.href = targetUrl; + } else if (access_token) { + localStorage.setItem('access_token', access_token); + router.push('/'); + } +} diff --git a/lunar/src/app/login/layout.tsx b/lunar/src/app/login/layout.tsx new file mode 100644 index 000000000..b096bfacf --- /dev/null +++ b/lunar/src/app/login/layout.tsx @@ -0,0 +1,18 @@ +'use client' + +export default function LoginLayout({ children }) { + return ( + + + + Login - Mega + + +
+ {children} +
+ + + ); + } + \ No newline at end of file diff --git a/lunar/src/app/login/page.tsx b/lunar/src/app/login/page.tsx new file mode 100644 index 000000000..9be2b0e76 --- /dev/null +++ b/lunar/src/app/login/page.tsx @@ -0,0 +1,72 @@ +'use client' + +export default function Login() { + + const apiUrl = process.env.NEXT_PUBLIC_API_URL; + const call_back_url = process.env.NEXT_PUBLIC_CALLBACK_URL; + + return ( + <> +
+
+ Mega + +

+ Sign in to your account with +

+
+ + +
+ + ) +}