diff --git a/Cargo.toml b/Cargo.toml index 3af0eebe3..58d3c9826 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 471fd5b11..7ffc71a80 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 4063ef193..a8ff3f26f 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 0dc70931b..d5d541f36 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, } @@ -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 ce5826380..6e65a3be1 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 4d82aecd8..b4f9a2fd5 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 000000000..ce7c26777 --- /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 3884c3c73..462b8a349 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 4f4791176..c937787d8 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 a0fddaeed..4287eca67 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, 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 { + 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, 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 { + 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 3adfdc102..c41b04380 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 4f5dfa12b..351417950 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 69134d013..316dc5fbb 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 d44f29b4d..15d3f7adb 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 for ListSSHKey { } } +#[derive(Debug, Serialize, Deserialize)] +pub struct ListToken { + pub id: i64, + pub token: String, + pub created_at: NaiveDateTime, +} + +impl From 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, diff --git a/mono/src/api/user/user_router.rs b/mono/src/api/user/user_router.rs index c40e2c111..ebb610a4b 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 { Router::new() @@ -20,6 +21,9 @@ pub fn routers() -> Router { .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, +) -> Result>, 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, + Path(key_id): Path, +) -> Result>, 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, +) -> Result>>, 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>, state: State, ) -> Result>, 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 000000000..724f38566 --- /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 1cd092e62..36b4f3654 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, params: InfoRefsParams, mut pack_protocol: SmartProtocol, ) -> Result, 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::().unwrap()); @@ -37,6 +47,47 @@ pub async fn git_info_refs( Ok(response) } +async fn http_auth(header: &HeaderMap, 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, 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, mut pack_protocol: SmartProtocol, ) -> Result, 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 fd1e1f138..d391ed6a6 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, Query(params): Query, uri: Uri, + req: Request, ) -> Result, 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 fc98b0b3e..9e36a20b2 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' }) My account + + + Tokens + 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 000000000..c42e90a71 --- /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} + Access Tokens + + + + +

{token}

+
+ + This is a list of access token associated with your account. Remove any keys that you do not recognize. +
+ + + + } + title={ + <> + {item.token} + + } + description={`Generated on ${format(new Date(item.created_at), "MMM dd,yyyy")}`} + /> + + } + /> + + + ) +} + + 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 000000000..d8acad72d --- /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 000000000..aa324822a --- /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 5385caea4..6b85b84d2 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 da06f2c79..bb69a2c60 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