Repository: web3infra-foundation/mega (language: rust)

Issue:
[Mono HTTP Server] Implement Git HTTP Protocol Authentication
The current mono Git HTTP server does not support authentication, so we have disabled HTTP push operations via a configuration. see issue #585 
In practice, when GitHub login is used, mono should send a GitHub token to the client for authentication during API calls such as info/refs. This would allow secure HTTP operations while enabling push functionality when authenticated via GitHub.


Discussion / hints:

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index 3af0eebe..58d3c982 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -71,7 +71,7 @@ bstr = "1.10.0"
 colored = "2.1.0"
 idgenerator = "2.0.0"
 num_cpus = "1.16.0"
-config = "0.14.0"
+config = "0.14.1"
 shadow-rs = "0.35.1"
 reqwest = "0.12.8"
 lazy_static = "1.5.0"
@@ -85,6 +85,8 @@ home = "0.5.9"
 ring = "0.17.8"
 cedar-policy = "4.2.1"
 secp256k1 = "0.30.0"
+oauth2 = "4.4.2"
+base64 = "0.22.1"
 
 [profile.release]
 debug = true
\ No newline at end of file
diff --git a/ceres/Cargo.toml b/ceres/Cargo.toml
index 471fd5b1..7ffc71a8 100644
--- a/ceres/Cargo.toml
+++ b/ceres/Cargo.toml
@@ -31,4 +31,4 @@ async-trait = { workspace = true }
 rand = { workspace = true }
 sea-orm = { workspace = true }
 ring = { workspace = true }
-hex ={ workspace = true}
+hex = { workspace = true }
diff --git a/ceres/src/protocol/smart.rs b/ceres/src/protocol/smart.rs
index 4063ef19..a8ff3f26 100644
--- a/ceres/src/protocol/smart.rs
+++ b/ceres/src/protocol/smart.rs
@@ -217,10 +217,10 @@ impl SmartProtocol {
             .await?;
 
         // do not block main thread here.
-        let ph_clone = pack_handler.clone();
+        let handler_clone = pack_handler.clone();
         let unpack_result = tokio::task::spawn_blocking(move || {
             let handle = tokio::runtime::Handle::current();
-            handle.block_on(async { ph_clone.handle_receiver(receiver).await })
+            handle.block_on(async { handler_clone.handle_receiver(receiver).await })
         })
         .await
         .unwrap();
diff --git a/common/src/config.rs b/common/src/config.rs
index 0dc70931..d5d541f3 100644
--- a/common/src/config.rs
+++ b/common/src/config.rs
@@ -210,6 +210,7 @@ impl Default for StorageConfig {
 pub struct MonoConfig {
     pub import_dir: PathBuf,
     pub disable_http_push: bool,
+    pub enable_http_auth: bool,
     pub admin: String,
     pub root_dirs: Vec<String>,
 }
@@ -219,6 +220,7 @@ impl Default for MonoConfig {
         Self {
             import_dir: PathBuf::from("/third-part"),
             disable_http_push: false,
+            enable_http_auth: false,
             admin: String::from("admin"),
             root_dirs: vec![
                 "third-part".to_string(),
diff --git a/docker/config.toml b/docker/config.toml
index ce582638..6e65a3be 100644
--- a/docker/config.toml
+++ b/docker/config.toml
@@ -56,6 +56,9 @@ import_dir = "/third-part"
 # The current mono Git HTTP server does not support authentication, so we provided a disabled operations here
 disable_http_push = false
 
+# Support http authtication, login in with github and generate token before push
+enable_http_auth = false
+
 # Set System Admin in directory init, replace the admin's github username here
 admin = "admin"
 
@@ -96,8 +99,8 @@ split_size = 20971520 # Default size is 20MB (20971520 bytes)
 github_client_id = ""
 github_client_secret = ""
 
-# Used redirect to ui after login
+# Used for redirect to ui after login, for example: https://console.gitmono.com
 ui_domain = "https://console.gitmono.com"
 
-# Set .gitmono.com on Production
+# Set your own domain here, for example: .gitmono.com
 cookie_domain = ".gitmono.com"
\ No newline at end of file
diff --git a/jupiter/Cargo.toml b/jupiter/Cargo.toml
index 4d82aecd..b4f9a2fd 100644
--- a/jupiter/Cargo.toml
+++ b/jupiter/Cargo.toml
@@ -32,6 +32,7 @@ serde_json = { workspace = true }
 idgenerator = { workspace = true }
 serde = { workspace = true }
 tokio = { workspace = true, features = ["macros"] }
+uuid = { workspace = true }
 
 [dev-dependencies]
 tokio = { workspace = true, features = ["macros"] }
diff --git a/jupiter/callisto/src/access_token.rs b/jupiter/callisto/src/access_token.rs
new file mode 100644
index 00000000..ce7c2677
--- /dev/null
+++ b/jupiter/callisto/src/access_token.rs
@@ -0,0 +1,19 @@
+//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
+
+use sea_orm::entity::prelude::*;
+
+#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
+#[sea_orm(table_name = "access_token")]
+pub struct Model {
+    #[sea_orm(primary_key, auto_increment = false)]
+    pub id: i64,
+    pub user_id: i64,
+    #[sea_orm(column_type = "Text")]
+    pub token: String,
+    pub created_at: DateTime,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {}
+
+impl ActiveModelBehavior for ActiveModel {}
diff --git a/jupiter/callisto/src/lib.rs b/jupiter/callisto/src/lib.rs
index 3884c3c7..462b8a34 100644
--- a/jupiter/callisto/src/lib.rs
+++ b/jupiter/callisto/src/lib.rs
@@ -1,7 +1,8 @@
-//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0
+//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
 
 pub mod prelude;
 
+pub mod access_token;
 pub mod db_enums;
 pub mod git_blob;
 pub mod git_commit;
diff --git a/jupiter/callisto/src/prelude.rs b/jupiter/callisto/src/prelude.rs
index 4f479117..c937787d 100644
--- a/jupiter/callisto/src/prelude.rs
+++ b/jupiter/callisto/src/prelude.rs
@@ -1,5 +1,6 @@
 //! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0
 
+pub use crate::access_token::Entity as AccessToken;
 pub use crate::git_blob::Entity as GitBlob;
 pub use crate::git_commit::Entity as GitCommit;
 pub use crate::git_issue::Entity as GitIssue;
diff --git a/jupiter/src/storage/user_storage.rs b/jupiter/src/storage/user_storage.rs
index a0fddaee..4287eca6 100644
--- a/jupiter/src/storage/user_storage.rs
+++ b/jupiter/src/storage/user_storage.rs
@@ -4,8 +4,9 @@ use sea_orm::{
     ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, IntoActiveModel, ModelTrait,
     QueryFilter,
 };
+use uuid::Uuid;
 
-use callisto::{ssh_keys, user};
+use callisto::{access_token, ssh_keys, user};
 use common::{errors::MegaError, utils::generate_id};
 
 #[derive(Clone)]
@@ -36,6 +37,14 @@ impl UserStorage {
         Ok(res)
     }
 
+    pub async fn find_user_by_name(&self, name: &str) -> Result<Option<user::Model>, MegaError> {
+        let res = user::Entity::find()
+            .filter(user::Column::Name.eq(name))
+            .one(self.get_connection())
+            .await?;
+        Ok(res)
+    }
+
     pub async fn save_user(&self, user: user::Model) -> Result<(), MegaError> {
         let a_model = user.into_active_model();
         a_model.insert(self.get_connection()).await.unwrap();
@@ -92,4 +101,60 @@ impl UserStorage {
             .await?;
         Ok(res)
     }
+
+    pub async fn generate_token(&self, user_id: i64) -> Result<String, MegaError> {
+        let token_str = Uuid::new_v4().to_string();
+        let model = access_token::Model {
+            id: generate_id(),
+            user_id,
+            token: token_str.clone(),
+            created_at: chrono::Utc::now().naive_utc(),
+        };
+        let a_model = model.into_active_model();
+        a_model.insert(self.get_connection()).await.unwrap();
+        Ok(token_str.to_owned())
+    }
+
+    pub async fn delete_token(&self, user_id: i64, id: i64) -> Result<(), MegaError> {
+        let res = access_token::Entity::find()
+            .filter(access_token::Column::Id.eq(id))
+            .filter(access_token::Column::UserId.eq(user_id))
+            .one(self.get_connection())
+            .await?;
+        if let Some(model) = res {
+            model.delete(self.get_connection()).await?;
+        }
+        Ok(())
+    }
+
+    pub async fn list_token(&self, user_id: i64) -> Result<Vec<access_token::Model>, MegaError> {
+        let res = access_token::Entity::find()
+            .filter(access_token::Column::UserId.eq(user_id))
+            .all(self.get_connection())
+            .await?;
+        Ok(res)
+    }
+
+    pub async fn check_token(&self, user_id: i64, token: &str) -> Result<bool, MegaError> {
+        let res = access_token::Entity::find()
+            .filter(access_token::Column::UserId.eq(user_id))
+            .filter(access_token::Column::Token.eq(token))
+            .one(self.get_connection())
+            .await?;
+        match res {
+            Some(_) => Ok(true),
+            None => Ok(false),
+        }
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use uuid::Uuid;
+
+    #[test]
+    fn token_format() {
+        let uuid = Uuid::new_v4().to_string();
+        println!("{:?}", uuid);
+    }
 }
diff --git a/mega/config.toml b/mega/config.toml
index 3adfdc10..c41b0438 100644
--- a/mega/config.toml
+++ b/mega/config.toml
@@ -56,6 +56,9 @@ import_dir = "/third-part"
 # The current mono Git HTTP server does not support authentication, so we provided a disabled operations here
 disable_http_push = false
 
+# Support http authtication, login in with github and generate token before push
+enable_http_auth = false
+
 # Set System Admin in directory init, replace the admin's github username here
 admin = "admin"
 
diff --git a/mono/Cargo.toml b/mono/Cargo.toml
index 4f5dfa12..35141795 100644
--- a/mono/Cargo.toml
+++ b/mono/Cargo.toml
@@ -53,7 +53,8 @@ ed25519-dalek = { workspace = true, features = ["pkcs8"] }
 lazy_static = { workspace = true }
 ctrlc = { workspace = true }
 shadow-rs = { workspace = true }
-oauth2 = "4.4.2"
+oauth2 = { workspace = true }
+base64 = { workspace = true }
 async-session = "3.0.0"
 http = "1.1.0"
 cedar-policy = { workspace = true }
@@ -62,3 +63,9 @@ cedar-policy = { workspace = true }
 
 [build-dependencies]
 shadow-rs = { workspace = true }
+
+# [target.'cfg(not(target_env = "msvc"))'.dependencies]
+# tikv-jemallocator = "0.6"
+
+# [target.'cfg(not(target_env = "msvc"))'.dependencies]
+# jemallocator = {version = "0.5.4", features = ["debug", "profiling"]}
\ No newline at end of file
diff --git a/mono/config.toml b/mono/config.toml
index 69134d01..316dc5fb 100644
--- a/mono/config.toml
+++ b/mono/config.toml
@@ -56,6 +56,9 @@ import_dir = "/third-part"
 # The current mono Git HTTP server does not support authentication, so we provided a disabled operations here
 disable_http_push = false
 
+# Support http authtication, login in with github and generate token before push
+enable_http_auth = false
+
 # Set System Admin in directory init, replace the admin's github username here
 admin = "admin"
 
@@ -96,8 +99,8 @@ split_size = 20971520 # Default size is 20MB (20971520 bytes)
 github_client_id = ""
 github_client_secret = ""
 
-# Used redirect to ui after login
+# Used for redirect to ui after login, for example: https://console.gitmono.com
 ui_domain = "http://localhost:3000"
 
-# Set .gitmono.com on Production
+# Set your own domain here, for example: .gitmono.com
 cookie_domain = "localhost"
\ No newline at end of file
diff --git a/mono/src/api/user/model.rs b/mono/src/api/user/model.rs
index d44f29b4..15d3f7ad 100644
--- a/mono/src/api/user/model.rs
+++ b/mono/src/api/user/model.rs
@@ -1,4 +1,4 @@
-use callisto::ssh_keys;
+use callisto::{access_token, ssh_keys};
 use chrono::NaiveDateTime;
 use serde::{Deserialize, Serialize};
 
@@ -29,6 +29,25 @@ impl From<ssh_keys::Model> for ListSSHKey {
     }
 }
 
+#[derive(Debug, Serialize, Deserialize)]
+pub struct ListToken {
+    pub id: i64,
+    pub token: String,
+    pub created_at: NaiveDateTime,
+}
+
+impl From<access_token::Model> for ListToken {
+    fn from(value: access_token::Model) -> Self {
+        let mut mask_token =  value.token;
+        mask_token.replace_range(7..32, "-******-");
+        Self {
+            id: value.id,
+            token: mask_token,
+            created_at: value.created_at,
+        }
+    }
+}
+
 #[derive(Debug, Serialize, Deserialize)]
 pub struct RepoPermissions {
     pub admin: Vec<String>,
diff --git a/mono/src/api/user/user_router.rs b/mono/src/api/user/user_router.rs
index c40e2c11..ebb610a4 100644
--- a/mono/src/api/user/user_router.rs
+++ b/mono/src/api/user/user_router.rs
@@ -13,6 +13,7 @@ use crate::api::user::model::AddSSHKey;
 use crate::api::user::model::ListSSHKey;
 use crate::api::MonoApiServiceState;
 use crate::api::{error::ApiError, oauth::model::LoginUser, util};
+use crate::api::user::model::ListToken;
 
 pub fn routers() -> Router<MonoApiServiceState> {
     Router::new()
@@ -20,6 +21,9 @@ pub fn routers() -> Router<MonoApiServiceState> {
         .route("/user/ssh", get(list_key))
         .route("/user/ssh", post(add_key))
         .route("/user/ssh/:key_id/delete", post(remove_key))
+        .route("/user/token/generate", post(generate_token))
+        .route("/user/token/list", get(list_token))
+        .route("/user/token/:key_id/delete", post(remove_token))
         .route("/repo-permissions", get(repo_permissions))
 }
 
@@ -96,12 +100,70 @@ async fn list_key(
     Ok(Json(res))
 }
 
+async fn generate_token(
+    user: LoginUser,
+    state: State<MonoApiServiceState>,
+) -> Result<Json<CommonResult<String>>, ApiError> {
+    let res = state
+        .context
+        .services
+        .user_storage
+        .generate_token(user.user_id)
+        .await;
+    let res = match res {
+        Ok(data) => CommonResult::success(Some(data)),
+        Err(err) => CommonResult::failed(&err.to_string()),
+    };
+    Ok(Json(res))
+}
+
+
+async fn remove_token(
+    user: LoginUser,
+    state: State<MonoApiServiceState>,
+    Path(key_id): Path<i64>,
+) -> Result<Json<CommonResult<String>>, ApiError> {
+    let res = state
+        .context
+        .services
+        .user_storage
+        .delete_token(user.user_id, key_id)
+        .await;
+    let res = match res {
+        Ok(_) => CommonResult::success(None),
+        Err(err) => CommonResult::failed(&err.to_string()),
+    };
+    Ok(Json(res))
+}
+
+
+async fn list_token(
+    user: LoginUser,
+    state: State<MonoApiServiceState>,
+) -> Result<Json<CommonResult<Vec<ListToken>>>, ApiError> {
+    let res = state
+        .context
+        .services
+        .user_storage
+        .list_token(user.user_id)
+        .await;
+    let res = match res {
+        Ok(data) => {
+            let res = data.into_iter().map(|x| x.into()).collect();
+            CommonResult::success(Some(res))
+        },
+        Err(err) => CommonResult::failed(&err.to_string()),
+    };
+    Ok(Json(res))
+}
+
 async fn repo_permissions(
     Query(query): Query<HashMap<String, String>>,
     state: State<MonoApiServiceState>,
 ) -> Result<Json<CommonResult<String>>, ApiError> {
     let path = std::path::PathBuf::from(query.get("path").unwrap());
     let _ = util::get_entitystore(path, state).await;
+    // TODO
     Ok(Json(CommonResult::success(Some(String::new()))))
 }
 
diff --git a/mono/src/git_protocol/AUTHENTICATION.MD b/mono/src/git_protocol/AUTHENTICATION.MD
new file mode 100644
index 00000000..724f3856
--- /dev/null
+++ b/mono/src/git_protocol/AUTHENTICATION.MD
@@ -0,0 +1,101 @@
+## Git HTTP Request Authentication Process
+
+When Git initiates an HTTP request, the authentication process primarily involves how credentials are passed to verify the user’s identity, ensuring access to remote Git repositories. Below is an overview of the authentication process when Git communicates over HTTP/HTTPS.
+
+### 1. Authentication Methods
+
+Git supports several common HTTP/HTTPS authentication methods:
+
+	•	Basic Authentication: Username and password/token are sent in the Authorization header encoded in Base64.
+	•	Personal Access Token (PAT): Typically used as a replacement for username and password, especially on services like GitHub, GitLab, etc.
+	•	OAuth: Allows for authentication using OAuth tokens, provided the service provider supports it.
+
+Usually, Git uses the Authorization header to carry authentication information, but with HTTPS encryption, the credentials are secure during transmission.
+
+### 2. Authentication Process
+
+(1) Initial Request: No Authentication Information
+
+When the Git client first sends an HTTP request to a remote repository, it typically does not immediately include the Authorization header. It sends a standard request to ask the server if the repository is available.
+
+Example request (without authentication):
+
+```http
+GET /path/to/repo.git/info/refs?service=git-upload-pack HTTP/1.1
+Host: git.gitmono.com
+User-Agent: git/2.46.0
+```
+
+(2) Server Response: Requires Authentication
+
+If the remote repository requires authentication to access, the server will respond with a 401 Unauthorized, including a WWW-Authenticate header, indicating that the client needs to authenticate.
+
+Example response:
+
+```http
+HTTP/1.1 401 Unauthorized
+WWW-Authenticate: Basic realm="Mega"
+```
+
+This informs the Git client that it needs to use Basic Authentication or another supported authentication method.
+
+(3) Client Sends Authentication Information
+
+The Git client will respond to the server’s 401 Unauthorized by looking for locally stored credentials (like username and password or Personal Access Token) or prompting the user for credential input. It will then resend the request, this time including the Authorization header with the authentication information.
+
+In Basic Authentication, Git will combine the username and password/token into a username:password format, encode it in Base64, and place it in the Authorization header.
+
+Example request:
+
+```http
+GET /path/to/repo.git/info/refs?service=git-upload-pack HTTP/1.1
+Host: git.gitmono.com
+User-Agent: git/2.46.0
+Authorization: Basic dXNlcm5hbWU6dG9rZW4=
+```
+
+Here, dXNlcm5hbWU6dG9rZW4= is the Base64 encoded username:token.
+
+(4) Server Validates Authentication Information
+
+The server will check the credentials in the Authorization header it received:
+
+	•	If authentication is successful, the server will return 200 OK and continue with the Git operation (like pull, push, etc.).
+	•	If authentication fails, the server will return 401 Unauthorized and may prompt the user to re-enter their credentials.
+
+(5) Request Ends
+
+Once the credential verification is successful, the Git client can interact with the remote repository. Credentials are typically cached for the session to avoid requiring re-authentication with each request.
+
+### 3. Credential Storage and Management
+
+(1) Credential Storage
+
+To avoid having to manually enter usernames and passwords each time, Git provides various credential storage methods:
+
+	•	Credential Helper: Git’s credential.helper allows users to save and cache credentials for future requests automatically.
+	•	credential.helper=cache: Caches credentials for a period.
+	•	credential.helper=osxkeychain: Uses the system’s keychain service to store credentials on macOS.
+	•	credential.helper=store: Saves credentials in plain text in the user’s directory (not recommended).
+
+git config --global credential.helper cache
+
+### 4. How to Debug Authentication Requests
+
+The following environment variables can be used to debug the Git HTTP request authentication process:
+
+	•	GIT_CURL_VERBOSE=1: Used for debugging Git HTTP requests, including HTTPS requests and authentication processes.
+	•	Although the Authorization header will be hidden for security reasons, you can check the response status codes (like 401 Unauthorized) to determine if authentication is successful.
+	•	GIT_TRACE=1: Used for debugging Git’s internal operations, including credential lookup and handling.
+
+Example Debug Command:
+
+```bash
+GIT_CURL_VERBOSE=1 GIT_TRACE=1 git push
+```
+
+### 5. Common Issues
+
+	•	Credentials Not Saved: If you find that Git prompts for a username and password every time, check whether credential.helper is configured correctly.
+	•	Authorization Header Not Displayed: For security reasons, the Git client does not display the full Authorization header in debug output, but it will send the correct header.
+	•	Token Expiration: If using a Personal Access Token (PAT), ensure that the token has not expired and has sufficient permissions for the requested Git operations.
diff --git a/mono/src/git_protocol/http.rs b/mono/src/git_protocol/http.rs
index 1cd092e6..36b4f365 100644
--- a/mono/src/git_protocol/http.rs
+++ b/mono/src/git_protocol/http.rs
@@ -3,8 +3,12 @@ use std::convert::Infallible;
 use anyhow::Result;
 use axum::body::Body;
 use axum::http::{HeaderValue, Request, Response};
+use base64::engine::general_purpose;
+use base64::prelude::*;
 use bytes::{Bytes, BytesMut};
 use futures::{stream, TryStreamExt};
+use http::HeaderMap;
+use jupiter::context::Context;
 use tokio::io::AsyncReadExt;
 use tokio_stream::StreamExt;
 
@@ -19,9 +23,15 @@ use common::model::InfoRefsParams;
 // where $servicename MUST be the service name the client wishes to contact to complete the operation.
 // The request MUST NOT contain additional query parameters.
 pub async fn git_info_refs(
+    req: Request<Body>,
     params: InfoRefsParams,
     mut pack_protocol: SmartProtocol,
 ) -> Result<Response<Body>, ProtocolError> {
+    if pack_protocol.context.config.monorepo.enable_http_auth
+        && !http_auth(req.headers(), &pack_protocol.context).await
+    {
+        return auth_failed();
+    }
     let service_name = params.service.unwrap();
     pack_protocol.service_type = Some(service_name.parse::<ServiceType>().unwrap());
 
@@ -37,6 +47,47 @@ pub async fn git_info_refs(
     Ok(response)
 }
 
+async fn http_auth(header: &HeaderMap<HeaderValue>, context: &Context) -> bool {
+    let stg = context.services.user_storage.clone();
+    for (k, v) in header {
+        if k == http::header::AUTHORIZATION {
+            let decoded = general_purpose::STANDARD
+                .decode(
+                    v.to_str()
+                        .unwrap()
+                        .strip_prefix("Basic ")
+                        .unwrap()
+                        .as_bytes(),
+                )
+                .unwrap();
+            let credentials = String::from_utf8(decoded).unwrap_or_default();
+            let mut parts = credentials.splitn(2, ':');
+            let username = parts.next().unwrap_or("");
+            let password = parts.next().unwrap_or("");
+            tracing::debug!("{}, {}", username, password);
+            match stg.find_user_by_name(username).await.unwrap() {
+                Some(user) => {
+                    return stg.check_token(user.id, password).await.unwrap();
+                }
+                None => return false,
+            }
+        }
+    }
+    false
+}
+
+fn auth_failed() -> Result<Response<Body>, ProtocolError> {
+    let resp = Response::builder()
+        .status(401)
+        .header(
+            http::header::WWW_AUTHENTICATE,
+            HeaderValue::from_static("Basic realm=Mega"),
+        )
+        .body(Body::empty())
+        .unwrap();
+    Ok(resp)
+}
+
 /// # Handles a Git upload pack request and prepares the response.
 ///
 /// The function takes a `req` parameter representing the HTTP request received and a `pack_protocol`
@@ -125,6 +176,11 @@ pub async fn git_receive_pack(
     req: Request<Body>,
     mut pack_protocol: SmartProtocol,
 ) -> Result<Response<Body>, ProtocolError> {
+    if pack_protocol.context.config.monorepo.enable_http_auth
+        && !http_auth(req.headers(), &pack_protocol.context).await
+    {
+        return auth_failed();
+    }
     // Convert the request body into a data stream.
     let mut data_stream = req.into_body().into_data_stream();
     let mut report_status = Bytes::new();
diff --git a/mono/src/server/https_server.rs b/mono/src/server/https_server.rs
index fd1e1f13..d391ed6a 100644
--- a/mono/src/server/https_server.rs
+++ b/mono/src/server/https_server.rs
@@ -133,7 +133,6 @@ pub async fn start_http(context: Context, options: HttpOptions) {
 ///   - POST       end of `Regex::new(r"/git-upload-pack$")`
 ///   - POST       end of `Regex::new(r"/git-receive-pack$")`
 pub async fn app(context: Context, host: String, port: u16, common: CommonOptions) -> Router {
-
     let state = AppState {
         host,
         port,
@@ -183,6 +182,7 @@ pub async fn get_method_router(
     state: State<AppState>,
     Query(params): Query<InfoRefsParams>,
     uri: Uri,
+    req: Request<Body>,
 ) -> Result<Response<Body>, ProtocolError> {
     if INFO_REFS_REGEX.is_match(uri.path()) {
         let pack_protocol = SmartProtocol::new(
@@ -190,7 +190,7 @@ pub async fn get_method_router(
             state.context.clone(),
             TransportProtocol::Http,
         );
-        crate::git_protocol::http::git_info_refs(params, pack_protocol).await
+        crate::git_protocol::http::git_info_refs(req, params, pack_protocol).await
     } else {
         Err(ProtocolError::NotFound(
             "Operation not supported".to_owned(),
diff --git a/moon/src/app/(dashboard)/application-layout.tsx b/moon/src/app/(dashboard)/application-layout.tsx
index fc98b0b3..9e36a20b 100644
--- a/moon/src/app/(dashboard)/application-layout.tsx
+++ b/moon/src/app/(dashboard)/application-layout.tsx
@@ -31,6 +31,7 @@ import {
   PlusIcon,
   ShieldCheckIcon,
   UserCircleIcon,
+  KeyIcon,
 } from '@heroicons/react/16/solid'
 import {
   Cog6ToothIcon,
@@ -51,6 +52,10 @@ function AccountDropdownMenu({ anchor }: { anchor: 'top start' | 'bottom end' })
         <UserCircleIcon />
         <DropdownLabel>My account</DropdownLabel>
       </DropdownItem>
+      <DropdownItem href="/user/token">
+        <KeyIcon />
+        <DropdownLabel>Tokens</DropdownLabel>
+      </DropdownItem>
       <DropdownDivider />
       <DropdownItem href="#">
         <ShieldCheckIcon />
diff --git a/moon/src/app/(dashboard)/user/token/page.tsx b/moon/src/app/(dashboard)/user/token/page.tsx
new file mode 100644
index 00000000..c42e90a7
--- /dev/null
+++ b/moon/src/app/(dashboard)/user/token/page.tsx
@@ -0,0 +1,132 @@
+'use client'
+
+import { Divider } from '@/components/catalyst/divider'
+import { Heading } from '@/components/catalyst/heading'
+import { useEffect, useState } from 'react'
+import { Flex, Button, message, List, Modal } from "antd";
+import {
+    KeyIcon,
+} from '@heroicons/react/20/solid'
+import { format } from 'date-fns'
+import copy from 'copy-to-clipboard';
+
+
+interface TokenItem {
+    id: number,
+    token: string,
+    created_at: string,
+}
+
+export default function KeysPage() {
+    const [keyList, setKeyList] = useState([]);
+    const [messageApi, contextHolder] = message.useMessage();
+    const [isModalOpen, setIsModalOpen] = useState(false);
+    const [token, setToken] = useState("");
+
+    const error = () => {
+        messageApi.open({
+            type: 'error',
+            content: 'Delete failed!',
+        });
+    };
+    const success = () => {
+        messageApi.open({
+            type: 'success',
+            content: 'Delete success!',
+        });
+    };
+
+    const generateToken = async () => {
+        try {
+            const res = await fetch(`/api/user/token`, {
+                method: 'POST',
+            });
+            const response = await res.json();
+            const token = response.data.data;
+            setToken(token);
+        } catch (error) {
+            console.error('Error fetching data:', error);
+        }
+    };
+
+    const showModal = () => {
+        setIsModalOpen(true);
+        generateToken();
+    };
+
+    const handleOk = () => {
+        setIsModalOpen(false);
+        copy(token)
+        fetchToken()
+    };
+
+    const handleCancel = () => {
+        setIsModalOpen(false);
+    };
+
+    const fetchToken = async () => {
+        try {
+            const res = await fetch(`/api/user/token`);
+            const response = await res.json();
+            const keyList = response.data.data;
+            setKeyList(keyList);
+        } catch (error) {
+            console.error('Error fetching data:', error);
+        }
+    };
+
+    useEffect(() => {
+        fetchToken();
+    }, []);
+
+    const delete_ssh_key = async (id) => {
+        const res = await fetch(`/api/user/token/${id}/delete`, {
+            method: 'POST',
+        });
+        if (res.ok) {
+            success();
+            fetchToken();
+        } else {
+            error();
+        }
+    }
+
+    return (
+        <>
+            {contextHolder}
+            <Heading>Access Tokens</Heading>
+            <Flex justify={'flex-end'} >
+                <Button style={{ backgroundColor: '#428646' }} onClick={showModal} >Generate Token</Button>
+            </Flex>
+            <Modal title="Generated Token, Only show once, Please Copy it before close" open={isModalOpen} onOk={handleOk} onCancel={handleCancel}>
+                <p>{token}</p>
+            </Modal>
+            <Divider className="my-10 mt-6" />
+            This is a list of access token associated with your account. Remove any keys that you do not recognize.
+            <br />
+            <List
+                size="large"
+                bordered
+                dataSource={keyList as TokenItem[]}
+                renderItem={(item) =>
+                    <List.Item>
+                        <List.Item.Meta
+                            avatar={
+                                <KeyIcon className="size-6" />
+                            }
+                            title={
+                                <>
+                                    {item.token}
+                                </>
+                            }
+                            description={`Generated on ${format(new Date(item.created_at), "MMM dd,yyyy")}`}
+                        />
+                        <Button danger onClick={() => delete_ssh_key(item.id)} >Delete</Button>
+                    </List.Item>}
+            />
+        </>
+
+    )
+}
+
+
diff --git a/moon/src/app/api/user/token/[id]/delete/route.ts b/moon/src/app/api/user/token/[id]/delete/route.ts
new file mode 100644
index 00000000..d8acad72
--- /dev/null
+++ b/moon/src/app/api/user/token/[id]/delete/route.ts
@@ -0,0 +1,26 @@
+import { verifySession } from '@/app/lib/dal'
+import { NextResponse } from 'next/server'
+
+const endpoint = process.env.MEGA_INTERNAL_HOST;
+
+export async function POST(request: Request, { params }: { params: { id: string } }) {
+    const session = await verifySession()
+    if (!session) return Response.json({})
+
+    const cookieHeader = request.headers.get('cookie') || '';
+
+    const res = await fetch(`${endpoint}/api/v1/user/token/${params.id}/delete`, {
+        headers: {
+            'Cookie': cookieHeader,
+        },
+        method: 'POST'
+    })
+    if (!res.ok) {
+        return new NextResponse(
+            JSON.stringify({ error: res.statusText }),
+            { status: res.status }
+        );
+    }
+    const data = await res.json()
+    return Response.json({ data })
+}
\ No newline at end of file
diff --git a/moon/src/app/api/user/token/route.ts b/moon/src/app/api/user/token/route.ts
new file mode 100644
index 00000000..aa324822
--- /dev/null
+++ b/moon/src/app/api/user/token/route.ts
@@ -0,0 +1,51 @@
+import { verifySession } from '@/app/lib/dal'
+import { NextResponse, type NextRequest } from 'next/server'
+export const revalidate = 0
+export const dynamic = 'force-dynamic' // defaults to auto
+
+const endpoint = process.env.MEGA_INTERNAL_HOST;
+
+export async function POST(request: NextRequest) {
+    const session = await verifySession()
+    if (!session) return Response.json({})
+
+    const cookieHeader = request.headers.get('cookie') || '';
+
+    const res = await fetch(`${endpoint}/api/v1/user/token/generate`, {
+        headers: {
+            'Cookie': cookieHeader,
+            'Content-Type': 'application/json',
+        },
+        method: 'POST',
+    })
+    if (!res.ok) {
+        return new NextResponse(
+            JSON.stringify({ error: res.statusText }),
+            { status: res.status }
+        );
+    }
+    const data = await res.json()
+    return Response.json({ data })
+}
+
+
+export async function GET(request: NextRequest) { 
+    const session = await verifySession()
+    if (!session) return Response.json({})
+
+    const cookieHeader = request.headers.get('cookie') || '';
+
+    const res = await fetch(`${endpoint}/api/v1/user/token/list`, {
+        headers: {
+            'Cookie': cookieHeader,
+        },
+    })
+    if (!res.ok) {
+        return new NextResponse(
+            JSON.stringify({ error: res.statusText }),
+            { status: res.status }
+        );
+    }
+    const data = await res.json()
+    return Response.json({ data })
+}
\ No newline at end of file
diff --git a/moon/src/middleware.ts b/moon/src/middleware.ts
index 5385caea..6b85b84d 100644
--- a/moon/src/middleware.ts
+++ b/moon/src/middleware.ts
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
 import { cookies } from 'next/headers'
 
 // 1. Specify protected and public routes
-const protectedRoutes = ['/user/keys/add', '/user/keys']
+const protectedRoutes = ['/user/keys/add', '/user/keys', 'user/token']
 const publicRoutes = ['/login', '/']
 
 export default async function middleware(req: NextRequest) {
diff --git a/saturn/mega_policies.cedar b/saturn/mega_policies.cedar
index da06f2c7..bb69a2c6 100644
--- a/saturn/mega_policies.cedar
+++ b/saturn/mega_policies.cedar
@@ -51,7 +51,6 @@ permit (
 )
 when { principal in resource.admins };
 
-
 // root admin can do anything
 permit (principal, action, resource)
-when { principal == User::"genedna" };
\ No newline at end of file
+when { principal == User::"genedna" || principal == User::"benjamin-747" };
\ No newline at end of file


Test patch: