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

Issue:
[Feature Request]: Add build-in accounts in `mega` for authentication tests
For example, maybe we can config build-in accounts in `config.toml`, which makes it easier to test authentication mechanism.


Discussion / hints:

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index bd8ea72a..c94b9a96 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -35,19 +35,19 @@ mega = { path = "mega" }
 mono = { path = "mono" }
 libra = { path = "libra" }
 
-anyhow = "1.0.90"
-serde = "1.0.214"
+anyhow = "1.0.93"
+serde = "1.0.215"
 serde_json = "1.0.132"
 tracing = "0.1.40"
 tracing-subscriber = "0.3.18"
 tracing-appender = "0.2"
-thiserror = "2.0.0"
+thiserror = "2.0.3"
 rand = "0.8.5"
 smallvec = "1.13.2"
-tokio = "1.41.0"
+tokio = "1.41.1"
 tokio-stream = "0.1.16"
 tokio-test = "0.4.4"
-clap = "4.5.20"
+clap = "4.5.21"
 async-trait = "0.1.83"
 async-stream = "0.3.6"
 bytes = "1.8.0"
@@ -66,8 +66,8 @@ tower-http = "0.6.1"
 tower = "0.5.1"
 hex = "0.4.3"
 sea-orm = "1.1.1"
-flate2 = "1.0.34"
-bstr = "1.10.0"
+flate2 = "1.0.35"
+bstr = "1.11.0"
 colored = "2.1.0"
 idgenerator = "2.0.0"
 num_cpus = "1.16.0"
@@ -80,10 +80,10 @@ regex = "1.11.1"
 ed25519-dalek = "2.1.1"
 ctrlc = "3.4.4"
 git2 = "0.19.0"
-tempfile = "3.13.0"
+tempfile = "3.14.0"
 home = "0.5.9"
 ring = "0.17.8"
-cedar-policy = "4.2.1"
+cedar-policy = "4.2.2"
 secp256k1 = "0.30.0"
 oauth2 = "4.4.2"
 base64 = "0.22.1"
diff --git a/ceres/src/pack/import_repo.rs b/ceres/src/pack/import_repo.rs
index 8d9c0941..95034db7 100644
--- a/ceres/src/pack/import_repo.rs
+++ b/ceres/src/pack/import_repo.rs
@@ -55,7 +55,7 @@ impl PackHandler for ImportRepo {
         self.find_head_hash(refs)
     }
 
-    async fn handle_receiver(&self, receiver: Receiver<Entry>) -> Result<(), GitError> {
+    async fn handle_receiver(&self, receiver: Receiver<Entry>) -> Result<String, GitError> {
         let storage = self.context.services.git_db_storage.clone();
         let mut entry_list = vec![];
         let mut join_tasks = vec![];
@@ -80,7 +80,7 @@ impl PackHandler for ImportRepo {
             .await
             .unwrap();
         self.attach_to_monorepo_parent().await.unwrap();
-        Ok(())
+        Ok(String::new())
     }
 
     async fn full_pack(&self) -> Result<ReceiverStream<Vec<u8>>, GitError> {
@@ -294,6 +294,11 @@ impl PackHandler for ImportRepo {
             .await
     }
 
+    async fn handle_mr(&self, _: &str) -> Result<(), GitError> {
+        // do nothing in import_repo
+        Ok(())
+    }
+
     async fn update_refs(&self, refs: &RefCommand) -> Result<(), GitError> {
         let storage = self.context.services.git_db_storage.clone();
         match refs.command_type {
diff --git a/ceres/src/pack/mod.rs b/ceres/src/pack/mod.rs
index dbf6a51a..87d057ba 100644
--- a/ceres/src/pack/mod.rs
+++ b/ceres/src/pack/mod.rs
@@ -38,7 +38,7 @@ pub mod monorepo;
 pub trait PackHandler: Send + Sync {
     async fn head_hash(&self) -> (String, Vec<Refs>);
 
-    async fn handle_receiver(&self, rx: Receiver<Entry>) -> Result<(), GitError>;
+    async fn handle_receiver(&self, rx: Receiver<Entry>) -> Result<String, GitError>;
 
     /// Asynchronously retrieves the full pack data for the specified repository path.
     /// This function collects commits and nodes from the storage and packs them into
@@ -63,6 +63,8 @@ pub trait PackHandler: Send + Sync {
         hashes: Vec<String>,
     ) -> Result<Vec<raw_blob::Model>, MegaError>;
 
+    async fn handle_mr(&self, title: &str) -> Result<(), GitError>;
+
     async fn update_refs(&self, refs: &RefCommand) -> Result<(), GitError>;
 
     async fn check_commit_exist(&self, hash: &str) -> bool;
diff --git a/ceres/src/pack/monorepo.rs b/ceres/src/pack/monorepo.rs
index 64acc3d9..fd057a60 100644
--- a/ceres/src/pack/monorepo.rs
+++ b/ceres/src/pack/monorepo.rs
@@ -15,7 +15,10 @@ use tokio::sync::mpsc;
 use tokio_stream::wrappers::ReceiverStream;
 
 use callisto::{db_enums::ConvType, raw_blob};
-use common::{errors::MegaError, utils::{self, MEGA_BRANCH_NAME}};
+use common::{
+    errors::MegaError,
+    utils::{self, MEGA_BRANCH_NAME},
+};
 use jupiter::{context::Context, storage::mr_storage::MrStorage};
 use mercury::internal::{object::ObjectTrait, pack::encode::PackEncoder};
 use mercury::{
@@ -119,33 +122,43 @@ impl PackHandler for MonoRepo {
         self.find_head_hash(refs)
     }
 
-    async fn handle_receiver(&self, receiver: Receiver<Entry>) -> Result<(), GitError> {
-        let storage = self.context.mr_stg();
-        let path_str = self.path.to_str().unwrap();
-
-        let unpack_res = self.save_entry(receiver).await;
-        match unpack_res {
-            Ok(title) => match storage.get_open_mr_by_path(path_str).await.unwrap() {
-                Some(mr) => {
-                    let mut mr = mr.into();
-                    self.handle_existing_mr(&mut mr, &storage).await
+    async fn handle_receiver(&self, receiver: Receiver<Entry>) -> Result<String, GitError> {
+        let storage = self.context.services.mono_storage.clone();
+        let mut entry_list = Vec::new();
+        let mut join_tasks = vec![];
+        let mut current_commit_id = String::new();
+        let mut mr_title = String::new();
+        for entry in receiver {
+            if current_commit_id.is_empty() {
+                if entry.obj_type == ObjectType::Commit {
+                    current_commit_id = entry.hash.to_plain_str();
+                    let commit = Commit::from_bytes(&entry.data, entry.hash).unwrap();
+                    mr_title = commit.format_message();
+                }
+            } else {
+                if entry.obj_type == ObjectType::Commit {
+                    return Err(GitError::CustomError(
+                        "only single commit support in each push".to_string(),
+                    ));
                 }
-                None => {
-                    let link: String = utils::generate_link();
-                    let mr = MergeRequest {
-                        path: path_str.to_owned(),
-                        from_hash: self.from_hash.clone(),
-                        to_hash: self.to_hash.clone(),
-                        link,
-                        title,
-                        ..Default::default()
-                    };
-                    storage.save_mr(mr.clone().into()).await.unwrap();
-                    Ok(())
+                if entry_list.len() >= 1000 {
+                    let stg_clone = storage.clone();
+                    let commit_id = current_commit_id.clone();
+                    let handle = tokio::spawn(async move {
+                        stg_clone.save_entry(&commit_id, entry_list).await.unwrap();
+                    });
+                    join_tasks.push(handle);
+                    entry_list = vec![];
                 }
-            },
-            Err(err) => Err(err),
+            }
+            entry_list.push(entry);
         }
+        join_all(join_tasks).await;
+        storage
+            .save_entry(&current_commit_id, entry_list)
+            .await
+            .unwrap();
+        Ok(mr_title)
     }
 
     // monorepo full pack should follow the shallow clone command 'git clone --depth=1'
@@ -305,6 +318,36 @@ impl PackHandler for MonoRepo {
             .await
     }
 
+    async fn handle_mr(&self, title: &str) -> Result<(), GitError> {
+        let storage = self.context.mr_stg();
+        let path_str = self.path.to_str().unwrap();
+
+        match storage.get_open_mr_by_path(path_str).await.unwrap() {
+            Some(mr) => {
+                let mut mr = mr.into();
+                self.handle_existing_mr(&mut mr, &storage).await
+            }
+            None => {
+                if self.from_hash == "0".repeat(40) {
+                    return Err(GitError::CustomError(String::from(
+                        "Can not init directory under monorepo directory!",
+                    )));
+                }
+                let link: String = utils::generate_link();
+                let mr = MergeRequest {
+                    path: path_str.to_owned(),
+                    from_hash: self.from_hash.clone(),
+                    to_hash: self.to_hash.clone(),
+                    link,
+                    title: title.to_string(),
+                    ..Default::default()
+                };
+                storage.save_mr(mr.clone().into()).await.unwrap();
+                Ok(())
+            }
+        }
+    }
+
     async fn update_refs(&self, _: &RefCommand) -> Result<(), GitError> {
         //do nothing in monorepo because we use mr to handle refs update
         Ok(())
@@ -366,43 +409,4 @@ impl MonoRepo {
             &to[..6]
         )
     }
-
-    async fn save_entry(&self, receiver: Receiver<Entry>) -> Result<String, GitError> {
-        let storage = self.context.services.mono_storage.clone();
-        let mut entry_list = Vec::new();
-        let mut join_tasks = vec![];
-        let mut current_commit_id = String::new();
-        let mut mr_title = String::new();
-        for entry in receiver {
-            if current_commit_id.is_empty() {
-                if entry.obj_type == ObjectType::Commit {
-                    current_commit_id = entry.hash.to_plain_str();
-                    let commit = Commit::from_bytes(&entry.data, entry.hash).unwrap();
-                    mr_title = commit.format_message();
-                }
-            } else {
-                if entry.obj_type == ObjectType::Commit {
-                    return Err(GitError::CustomError(
-                        "only single commit support in each push".to_string(),
-                    ));
-                }
-                if entry_list.len() >= 1000 {
-                    let stg_clone = storage.clone();
-                    let commit_id = current_commit_id.clone();
-                    let handle = tokio::spawn(async move {
-                        stg_clone.save_entry(&commit_id, entry_list).await.unwrap();
-                    });
-                    join_tasks.push(handle);
-                    entry_list = vec![];
-                }
-            }
-            entry_list.push(entry);
-        }
-        join_all(join_tasks).await;
-        storage
-            .save_entry(&current_commit_id, entry_list)
-            .await
-            .unwrap();
-        Ok(mr_title)
-    }
 }
diff --git a/ceres/src/protocol/smart.rs b/ceres/src/protocol/smart.rs
index a8ff3f26..bef7dda3 100644
--- a/ceres/src/protocol/smart.rs
+++ b/ceres/src/protocol/smart.rs
@@ -241,7 +241,10 @@ impl SmartProtocol {
                 // b.The reference being pushed could be a non-fast-forward reference and the update hooks or configuration could be set to not allow that, etc.
                 // c.Also, some references can be updated while others can be rejected.
                 match unpack_result {
-                    Ok(_) => {
+                    Ok(ref title) => {
+                        if let Err(e) = pack_handler.handle_mr(title).await {
+                            command.failed(e.to_string());
+                        }
                         if !default_exist {
                             command.default_branch = true;
                             default_exist = true;
diff --git a/common/src/config.rs b/common/src/config.rs
index 72605388..1d16836d 100644
--- a/common/src/config.rs
+++ b/common/src/config.rs
@@ -16,6 +16,7 @@ pub struct Config {
     pub storage: StorageConfig,
     pub monorepo: MonoConfig,
     pub pack: PackConfig,
+    pub authentication: AuthConfig,
     pub lfs: LFSConfig,
     // Not used in mega app
     #[serde(default)]
@@ -209,7 +210,6 @@ impl Default for StorageConfig {
 #[derive(Serialize, Deserialize, Debug, Clone)]
 pub struct MonoConfig {
     pub import_dir: PathBuf,
-    pub enable_http_auth: bool,
     pub admin: String,
     pub root_dirs: Vec<String>,
 }
@@ -218,7 +218,6 @@ impl Default for MonoConfig {
     fn default() -> Self {
         Self {
             import_dir: PathBuf::from("/third-part"),
-            enable_http_auth: false,
             admin: String::from("admin"),
             root_dirs: vec![
                 "third-part".to_string(),
@@ -230,6 +229,25 @@ impl Default for MonoConfig {
     }
 }
 
+#[derive(Serialize, Deserialize, Debug, Clone)]
+pub struct AuthConfig {
+    pub enable_http_auth: bool,
+    pub enable_test_user: bool,
+    pub test_user_name: String,
+    pub test_user_token: String,
+}
+
+impl Default for AuthConfig {
+    fn default() -> Self {
+        Self {
+            enable_http_auth: false,
+            enable_test_user: false,
+            test_user_name: String::from("mega"),
+            test_user_token: String::from("mega")
+        }
+    }
+}
+
 #[derive(Serialize, Deserialize, Debug, Clone)]
 pub struct PackConfig {
     pub pack_decode_mem_size: usize,
diff --git a/docker/config.toml b/docker/config.toml
index 5166360e..3e9b3bbb 100644
--- a/docker/config.toml
+++ b/docker/config.toml
@@ -47,15 +47,27 @@ obs_region = "cn-east-3"
 # Override the endpoint URL used for remote storage services
 obs_endpoint = "https://obs.cn-east-3.myhuaweicloud.com"
 
+[authentication]
+# Support http authentication, login in with github and generate token before push
+enable_http_auth = false
+
+# Enable a test user for debugging and development purposes.
+# If set to true, the service allows using a predefined test user for authentication.
+enable_test_user = true
+
+# Specify the name of the test user.
+# This is only relevant if `enable_test_user` is set to true.
+test_user_name = "mega"
+
+# Specify the token for the test user.
+# This is used for authentication when `enable_test_user` is set to true.
+test_user_token = "mega"
 
 [monorepo]
 ## Only import directory support multi-branch commit and tag, monorepo only support main branch
 ## Mega treats files under this directory as import repo and other directories as monorepo
 import_dir = "/third-part"
 
-# Support http authentication, 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/mega/config.toml b/mega/config.toml
index 1fe5771f..682c17f2 100644
--- a/mega/config.toml
+++ b/mega/config.toml
@@ -47,15 +47,27 @@ obs_region = "cn-east-3"
 # Override the endpoint URL used for remote storage services
 obs_endpoint = "https://obs.cn-east-3.myhuaweicloud.com"
 
+[authentication]
+# Support http authentication, login in with github and generate token before push
+enable_http_auth = false
+
+# Enable a test user for debugging and development purposes.
+# If set to true, the service allows using a predefined test user for authentication.
+enable_test_user = true
+
+# Specify the name of the test user.
+# This is only relevant if `enable_test_user` is set to true.
+test_user_name = "mega"
+
+# Specify the token for the test user.
+# This is used for authentication when `enable_test_user` is set to true.
+test_user_token = "mega"
 
 [monorepo]
 ## Only import directory support multi-branch commit and tag, monorepo only support main branch
 ## Mega treats files under this directory as import repo and other directories as monorepo
 import_dir = "/third-part"
 
-# Support http authentication, 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/config.toml b/mono/config.toml
index 37a50f38..714d8584 100644
--- a/mono/config.toml
+++ b/mono/config.toml
@@ -47,15 +47,27 @@ obs_region = "cn-east-3"
 # Override the endpoint URL used for remote storage services
 obs_endpoint = "https://obs.cn-east-3.myhuaweicloud.com"
 
+[authentication]
+# Support http authentication, login in with github and generate token before push
+enable_http_auth = false
+
+# Enable a test user for debugging and development purposes.
+# If set to true, the service allows using a predefined test user for authentication.
+enable_test_user = true
+
+# Specify the name of the test user.
+# This is only relevant if `enable_test_user` is set to true.
+test_user_name = "mega"
+
+# Specify the token for the test user.
+# This is used for authentication when `enable_test_user` is set to true.
+test_user_token = "mega"
 
 [monorepo]
 ## Only import directory support multi-branch commit and tag, monorepo only support main branch
 ## Mega treats files under this directory as import repo and other directories as monorepo
 import_dir = "/third-part"
 
-# Support http authentication, 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/src/api/mr/mod.rs b/mono/src/api/mr/mod.rs
index bfe28e2a..22cfd909 100644
--- a/mono/src/api/mr/mod.rs
+++ b/mono/src/api/mr/mod.rs
@@ -23,7 +23,7 @@ impl From<mega_mr::Model> for MrInfoItem {
     fn from(value: mega_mr::Model) -> Self {
         Self {
             link: value.link,
-            title: String::new(),
+            title: value.title,
             status: value.status.to_string(),
             open_timestamp: value.created_at.and_utc().timestamp(),
             merge_timestamp: value.merge_date.map(|dt| dt.and_utc().timestamp()),
diff --git a/mono/src/git_protocol/http.rs b/mono/src/git_protocol/http.rs
index 735c8aa0..9b1b3634 100644
--- a/mono/src/git_protocol/http.rs
+++ b/mono/src/git_protocol/http.rs
@@ -23,15 +23,9 @@ 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());
 
@@ -62,11 +56,27 @@ async fn http_auth(header: &HeaderMap<HeaderValue>, context: &Context) -> bool {
             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 context.user_stg().find_user_by_name(username).await.unwrap() {
+            let token = parts.next().unwrap_or("");
+            tracing::debug!("{}, {}", username, token);
+            let auth_config = context.config.authentication.clone();
+            if auth_config.enable_test_user
+                && username == auth_config.test_user_name
+                && token == auth_config.test_user_token
+            {
+                return true;
+            }
+            match context
+                .user_stg()
+                .find_user_by_name(username)
+                .await
+                .unwrap()
+            {
                 Some(user) => {
-                    return context.user_stg().check_token(user.id, password).await.unwrap();
+                    return context
+                        .user_stg()
+                        .check_token(user.id, token)
+                        .await
+                        .unwrap();
                 }
                 None => return false,
             }
@@ -175,7 +185,7 @@ 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
+    if pack_protocol.context.config.authentication.enable_http_auth
         && !http_auth(req.headers(), &pack_protocol.context).await
     {
         return auth_failed();
diff --git a/mono/src/server/https_server.rs b/mono/src/server/https_server.rs
index d14503a0..155f595a 100644
--- a/mono/src/server/https_server.rs
+++ b/mono/src/server/https_server.rs
@@ -182,7 +182,6 @@ 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 +189,7 @@ pub async fn get_method_router(
             state.context.clone(),
             TransportProtocol::Http,
         );
-        crate::git_protocol::http::git_info_refs(req, params, pack_protocol).await
+        crate::git_protocol::http::git_info_refs(params, pack_protocol).await
     } else {
         Err(ProtocolError::NotFound(
             "Operation not supported".to_owned(),
diff --git a/moon/package.json b/moon/package.json
index c462d30b..1978f775 100644
--- a/moon/package.json
+++ b/moon/package.json
@@ -10,33 +10,33 @@
   },
   "dependencies": {
     "@ant-design/icons": "^5.5.1",
-    "@ant-design/nextjs-registry": "^1.0.1",
-    "@headlessui/react": "^2.1.8",
+    "@ant-design/nextjs-registry": "^1.0.2",
+    "@headlessui/react": "^2.2.0",
     "@headlessui/tailwindcss": "^0.2.1",
-    "@heroicons/react": "^2.1.5",
-    "@lexical/react": "^0.19.0",
+    "@heroicons/react": "^2.2.0",
+    "@lexical/react": "^0.20.0",
     "@tailwindcss/forms": "^0.5.9",
     "clsx": "^2.1.1",
     "copy-to-clipboard": "^3.3.3",
     "date-fns": "^4.1.0",
-    "framer-motion": "^11.11.10",
-    "github-markdown-css": "^5.7.0",
+    "framer-motion": "^11.11.17",
+    "github-markdown-css": "^5.8.0",
     "highlight.js": "^11.10.0",
-    "lexical": "^0.19.0",
-    "next": "^15.0.2",
+    "lexical": "^0.20.0",
+    "next": "^15.0.3",
     "prism-react-renderer": "^2.4.0",
     "react": "^18.3.1",
     "react-dom": "^18.3.1",
     "react-markdown": "^9.0.1"
   },
   "devDependencies": {
-    "@types/node": "^22",
-    "@types/react": "^18.3.3",
+    "@types/node": "^22.9.1",
+    "@types/react": "^18.3.12",
     "@types/react-dom": "^18.3.0",
     "autoprefixer": "^10.4.19",
-    "eslint": "^8.57.0",
-    "eslint-config-next": "^15.0.1",
-    "postcss": "^8.4.40",
-    "typescript": "^5.5.0"
+    "eslint": "^9.15.0",
+    "eslint-config-next": "^15.0.3",
+    "postcss": "^8.4.49",
+    "typescript": "^5.6.3"
   }
 }
diff --git a/moon/src/app/(dashboard)/issue/[id]/page.tsx b/moon/src/app/(dashboard)/issue/[id]/page.tsx
index 3d538729..6cf3bf38 100644
--- a/moon/src/app/(dashboard)/issue/[id]/page.tsx
+++ b/moon/src/app/(dashboard)/issue/[id]/page.tsx
@@ -1,5 +1,5 @@
 'use client'
-import { useEffect, useState } from "react";
+import { useCallback, useEffect, useState } from "react";
 import { Card, Button, List, Tabs, TabsProps, Space, Timeline, Flex } from 'antd/lib';
 import { CommentOutlined, MergeOutlined, CloseCircleOutlined } from '@ant-design/icons';
 import RichEditor from "@/components/rich-editor/RichEditor";
@@ -37,11 +37,11 @@ export default function IssueDetailPage({ params }: { params: Params }) {
     const [loadings, setLoadings] = useState<boolean[]>([]);
     const router = useRouter();
 
-    const fetchDetail = async () => {
+    const fetchDetail = useCallback(async () => {
         const detail = await fetch(`/api/issue/${id}/detail`);
         const detail_json = await detail.json();
         setInfo(detail_json.data.data);
-    };
+    }, [id]);
 
     const checkLogin = async () => {
         const res = await fetch(`/api/auth`);
@@ -51,7 +51,7 @@ export default function IssueDetailPage({ params }: { params: Params }) {
     useEffect(() => {
         checkLogin()
         fetchDetail()
-    }, [id]);
+    }, [id, fetchDetail]);
 
     const set_to_loading = (index: number) => {
         setLoadings((prevLoadings) => {
diff --git a/moon/src/app/(dashboard)/issue/page.tsx b/moon/src/app/(dashboard)/issue/page.tsx
index 54c1c8ba..ce60527a 100644
--- a/moon/src/app/(dashboard)/issue/page.tsx
+++ b/moon/src/app/(dashboard)/issue/page.tsx
@@ -1,6 +1,6 @@
 'use client'
 import { Heading } from '@/components/catalyst/heading'
-import { useEffect, useState } from 'react';
+import { useCallback, useEffect, useState } from 'react';
 import React from 'react';
 import { Flex, List, PaginationProps, Tag, Button, Tabs, Space, TabsProps, Segmented } from 'antd/lib';
 import { format, formatDistance, fromUnixTime } from 'date-fns'
@@ -23,7 +23,7 @@ export default function IssuePage() {
     const [pageSize, setPageSize] = useState(10);
     const [status, setStatus] = useState("open")
 
-    const fetchData = async (page: number, per_page: number) => {
+    const fetchData = useCallback(async (page: number, per_page: number) => {
         try {
             const res = await fetch(`/api/issue/list`, {
                 method: 'POST',
@@ -47,11 +47,11 @@ export default function IssuePage() {
         } catch (error) {
             console.error('Error fetching data:', error);
         }
-    };
+    }, [status]);
 
     useEffect(() => {
         fetchData(1, pageSize);
-    }, [pageSize, status]);
+    }, [pageSize, status, fetchData]);
 
     const getStatusTag = (status: string) => {
         switch (status) {
diff --git a/moon/src/app/(dashboard)/mr/[id]/page.tsx b/moon/src/app/(dashboard)/mr/[id]/page.tsx
index 757d9601..3f0343b2 100644
--- a/moon/src/app/(dashboard)/mr/[id]/page.tsx
+++ b/moon/src/app/(dashboard)/mr/[id]/page.tsx
@@ -1,5 +1,5 @@
 'use client'
-import { useEffect, useState } from "react";
+import { useCallback, useEffect, useState } from "react";
 import { Card, Button, List, Tabs, TabsProps, Space, Timeline, Flex } from 'antd/lib';
 import { CommentOutlined, MergeOutlined, CloseCircleOutlined, PullRequestOutlined } from '@ant-design/icons';
 import { formatDistance, fromUnixTime } from 'date-fns';
@@ -44,13 +44,13 @@ export default function MRDetailPage({ params }: { params: Params }) {
         setLogin(res.ok);
     };
 
-    const fetchDetail = async () => {
+    const fetchDetail = useCallback(async () => {
         const detail = await fetch(`/api/mr/${id}/detail`);
         const detail_json = await detail.json();
         setMrDetail(detail_json.data.data);
-    };
+    }, [id]);
 
-    const fetchFileList = async () => {
+    const fetchFileList = useCallback(async () => {
         set_to_loading(2)
         try {
             const res = await fetch(`/api/mr/${id}/files`);
@@ -59,13 +59,13 @@ export default function MRDetailPage({ params }: { params: Params }) {
         } finally {
             cancel_loading(2)
         }
-    };
+    }, [id]);
 
     useEffect(() => {
         fetchDetail()
         fetchFileList();
         checkLogin();
-    }, [id]);
+    }, [id, fetchDetail, fetchFileList]);
 
     const set_to_loading = (index: number) => {
         setLoadings((prevLoadings) => {
diff --git a/moon/src/app/(dashboard)/mr/page.tsx b/moon/src/app/(dashboard)/mr/page.tsx
index dbfb76a6..bd786a6b 100644
--- a/moon/src/app/(dashboard)/mr/page.tsx
+++ b/moon/src/app/(dashboard)/mr/page.tsx
@@ -1,5 +1,5 @@
 'use client'
-import { useEffect, useState } from 'react';
+import { useCallback, useEffect, useState } from 'react';
 import React from 'react';
 import { List, PaginationProps, Tag, Tabs, TabsProps } from 'antd/lib';
 import { format, formatDistance, fromUnixTime } from 'date-fns'
@@ -23,7 +23,7 @@ export default function MergeRequestPage() {
     const [pageSize, setPageSize] = useState(10);
     const [status, setStatus] = useState("open")
 
-    const fetchData = async (page: number, per_page: number) => {
+    const fetchData = useCallback(async (page: number, per_page: number) => {
         try {
             const res = await fetch(`/api/mr/list`, {
                 method: 'POST',
@@ -47,12 +47,11 @@ export default function MergeRequestPage() {
         } catch (error) {
             console.error('Error fetching data:', error);
         }
-    };
+    }, [status]);
 
     useEffect(() => {
-
         fetchData(1, pageSize);
-    }, [pageSize, status]);
+    }, [pageSize, status, fetchData]);
 
     const getStatusTag = (status: string) => {
         switch (status) {
@@ -132,7 +131,7 @@ export default function MergeRequestPage() {
                             avatar={
                                 getStatusIcon(item.status)
                             }
-                            title={<Link href={`/mr/${item.link}`}>{`MR ${item.link} open by Mega automacticlly${item.title}`}{getStatusTag(item.status)}</Link>}
+                            title={<Link href={`/mr/${item.link}`}>{`${item.title}`} {getStatusTag(item.status)}</Link>}
                             description={getDescription(item)}
                         />
                     </List.Item>
diff --git a/moon/src/components/rich-editor/ExampleTheme.ts b/moon/src/components/rich-editor/ExampleTheme.ts
index bbd871b6..7b5f09c7 100644
--- a/moon/src/components/rich-editor/ExampleTheme.ts
+++ b/moon/src/components/rich-editor/ExampleTheme.ts
@@ -5,7 +5,7 @@
  * LICENSE file in the root directory of this source tree.
  *
  */
-export default {
+const theme = {
   code: 'editor-code',
   heading: {
     h1: 'editor-heading-h1',
@@ -40,3 +40,5 @@ export default {
     underlineStrikethrough: 'editor-text-underlineStrikethrough',
   },
 };
+
+export default theme;
\ No newline at end of file


Test patch: