Skip to content

Commit 0dc48aa

Browse files
Merge pull request #2 from dipamsen/read-env-var
See environment variables for a project
2 parents 22d8c9d + 8067cbc commit 0dc48aa

File tree

19 files changed

+380
-144
lines changed

19 files changed

+380
-144
lines changed

.maint

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
backend

README.md

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,47 @@
5252
## About The Project
5353
<div align="center">
5454
<a href="https://github.com/metakgp/maintos">
55-
<img width="80%" alt="image" src="https://user-images.githubusercontent.com/86282911/206632547-a3b34b47-e7ae-4186-a1e6-ecda7ddb38e6.png">
55+
<img width="80%" alt="image" src="https://gist.github.com/user-attachments/assets/e5f7e679-b7d4-413f-a874-2de638461780">
5656
</a>
5757
</div>
5858

59-
_Detailed explaination of the project goes here_
59+
_Maintos_ is a maintainer's dashboard which gives maintainers access to information and control on their projects, without requiring explicit access to the server. Maintainers of a project can start/stop the running containers/services, read logs for the project, as well as see and update environment variables.
6060

6161
<p align="right">(<a href="#top">back to top</a>)</p>
6262

6363
## Development
64-
[WIP]
64+
65+
1. Clone this repository.
66+
2. Backend:
67+
- Copy `.env.template` to `.env` and update the values as per [Environment Variables](#environment-variables).
68+
- For the backend to run, Docker must be installed and running.
69+
- Run the backend:
70+
```bash
71+
cargo run
72+
```
73+
3. Frontend:
74+
- Set the environment variables in `.env.local`:
75+
- `VITE_BACKEND_URL`: URL of the backend
76+
- `VITE_GH_OAUTH_CLIENT_ID`: Client ID of the GitHub OAuth App.
77+
- Run the frontend:
78+
```bash
79+
npm install
80+
npm run dev
81+
```
82+
83+
84+
### Environment Variables
85+
86+
This project needs a [GitHub OAuth app](https://github.com/settings/developers) and a [Personal Access Token](https://github.com/settings/personal-access-tokens) of an admin of the GitHub org.
87+
88+
- `GH_CLIENT_ID`, `GH_CLIENT_SECRET`: Client ID and Client Secret for the GitHub OAuth application.
89+
- `GH_ORG_NAME`: Name of the GitHub organisation
90+
- `GH_ORG_ADMIN_TOKEN`: A GitHub PAT of an org admin
91+
- `JWT_SECRET`: A secure string (for signing JWTs)
92+
- `DEPLOYMENTS_DIR`: Absolute path to directory containing all the project git repos (deployed)
93+
- `SERVER_PORT`: Port where the backend server listens to
94+
- `CORS_ALLOWED_ORIGINS`: Frontend URLs
95+
6596

6697
## Deployment
6798
[WIP]
@@ -98,12 +129,12 @@ See https://wiki.metakgp.org/w/Metakgp:Project_Maintainer.
98129
- [Harsh Khandeparkar](https://github.com/harshkhandeparkar)
99130
- [Devansh Gupta](https://github.com/Devansh-bit)
100131

101-
### Past Maintainer(s)
132+
<!-- ### Past Maintainer(s)
102133

103134
Previous maintainer(s) of this project.
104135
See https://wiki.metakgp.org/w/Metakgp:Project_Maintainer.
105136

106-
<p align="right">(<a href="#top">back to top</a>)</p>
137+
<p align="right">(<a href="#top">back to top</a>)</p> -->
107138

108139
## Additional documentation
109140

backend/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ http = "1.3.1"
1414
jwt = "0.16.0"
1515
reqwest = { version = "0.12.24", features = ["json"] }
1616
serde = { version = "1.0.228", features = ["derive"] }
17-
serde_json = "1.0.145"
17+
serde_json = { version = "1.0.145", features = ["preserve_order"] }
1818
sha2 = "0.10.9"
1919
tokio = { version = "1.48.0", features = ["full"] }
2020
tower-http = { version = "0.6.6", features = ["cors", "trace"] }

backend/src/routing/handlers.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ use axum::extract::State;
1010
use axum::{Extension, extract::Json, http::StatusCode};
1111
use serde::Deserialize;
1212
use serde::Serialize;
13+
use serde_json::Value;
1314

1415
use crate::auth::{self, Auth};
15-
use crate::utils::{Deployment, get_deployments};
16+
use crate::utils::{Deployment, get_deployments, get_env};
1617

1718
use super::{AppError, BackendResponse, RouterState};
1819

@@ -87,3 +88,29 @@ pub async fn deployments(
8788
get_deployments(&state.env_vars, &auth.username).await?,
8889
))
8990
}
91+
92+
#[derive(Deserialize)]
93+
/// The request format for the get environment variables endpoint
94+
pub struct EnvVarsReq {
95+
project_name: String,
96+
}
97+
98+
/// Gets the environment variables for a project if the user has access to it
99+
pub async fn get_env_vars(
100+
State(state): HandlerState,
101+
Extension(auth): Extension<Auth>,
102+
Json(body): Json<EnvVarsReq>,
103+
) -> HandlerReturn<Value> {
104+
let project_name = body.project_name.as_str();
105+
if let Ok(env_vars) = get_env(&state.env_vars, &auth.username, project_name).await {
106+
return Ok(BackendResponse::ok(
107+
"Successfully fetched environment variables.".into(),
108+
env_vars,
109+
));
110+
} else {
111+
return Ok(BackendResponse::error(
112+
"Error: Project not found or access denied.".into(),
113+
StatusCode::NOT_FOUND,
114+
));
115+
}
116+
}

backend/src/routing/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ pub fn get_router(env_vars: &EnvVars, docker: Arc<Docker>) -> axum::Router {
2626
axum::Router::new()
2727
.route("/profile", axum::routing::get(handlers::profile))
2828
.route("/deployments", axum::routing::get(handlers::deployments))
29+
.route("/get_env", axum::routing::post(handlers::get_env_vars))
2930
.route_layer(axum::middleware::from_fn_with_state(
3031
state.clone(),
3132
middleware::verify_jwt_middleware,

backend/src/utils.rs

Lines changed: 102 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
use std::str::FromStr;
1+
use std::{path::PathBuf, str::FromStr};
22

33
use anyhow::anyhow;
44
use git2::Repository;
55
use reqwest::Url;
66
use serde::{Deserialize, Serialize};
7+
use serde_json::{Map, Value};
78
use tokio::fs;
89

910
use crate::{env::EnvVars, github};
@@ -19,76 +20,119 @@ pub struct Deployment {
1920
repo_name: String,
2021
}
2122

23+
/// Check if a user has permission to access a project
24+
pub async fn check_access(env_vars: &EnvVars, username: &str, project_name: &str) -> Res<Deployment> {
25+
let deployments_dir = &env_vars.deployments_dir;
26+
27+
let client = reqwest::Client::new();
28+
let repo_path = format!("{}/{}", deployments_dir.display(), project_name);
29+
let repo = Repository::open(repo_path)?;
30+
let repo_url = repo
31+
.find_remote("origin")?
32+
.url()
33+
.ok_or(anyhow!(
34+
"Error: Origin remote URL not found for repo {project_name}."
35+
))?
36+
.to_string();
37+
let parsed_url = Url::from_str(&repo_url)?;
38+
let mut url_paths = parsed_url
39+
.path_segments()
40+
.ok_or(anyhow!("Error parsing repository remote URL."))?;
41+
let repo_owner = url_paths
42+
.next()
43+
.ok_or(anyhow!(
44+
"Error parsing repository remote URL: Repo owner not found."
45+
))?
46+
.to_string();
47+
let repo_name = url_paths
48+
.next()
49+
.ok_or(anyhow!(
50+
"Error parsing repository remote URL: Repo name not found."
51+
))?
52+
.to_string();
53+
if repo_owner == env_vars.gh_org_name {
54+
let collab_role = github::get_collaborator_role(
55+
&client,
56+
&env_vars.gh_org_admin_token,
57+
&repo_owner,
58+
&repo_name,
59+
username,
60+
)
61+
.await?;
62+
63+
// `None` means the user is not a collaborator
64+
if let Some(role) = collab_role.as_deref()
65+
&& (role == "maintain" || role == "admin")
66+
{
67+
return Ok(Deployment {
68+
name: project_name.to_string(),
69+
repo_url,
70+
repo_owner,
71+
repo_name,
72+
});
73+
}
74+
}
75+
Err(anyhow!("User does not have permission to access this project."))
76+
}
77+
2278
/// Get a list of deployments
2379
pub async fn get_deployments(env_vars: &EnvVars, username: &str) -> Res<Vec<Deployment>> {
2480
let deployments_dir = &env_vars.deployments_dir;
2581

2682
let mut deployments = Vec::new();
2783

28-
// To be reused for collaborator permission checking requests
29-
let client = reqwest::Client::new();
30-
3184
let mut dir_iter = fs::read_dir(deployments_dir).await?;
3285
while let Some(path) = dir_iter.next_entry().await? {
3386
if path.file_type().await?.is_dir()
34-
&& let Ok(repo) = Repository::open(path.path())
3587
{
36-
let name = path
37-
.file_name()
38-
.into_string()
39-
.map_err(|err| anyhow!("{}", err.display()))?;
40-
41-
let repo_url = repo
42-
.find_remote("origin")?
43-
.url()
44-
.ok_or(anyhow!(
45-
"Error: Origin remote URL not found for repo {name}."
46-
))?
47-
.to_string();
48-
49-
let parsed_url = Url::from_str(&repo_url)?;
50-
let mut url_paths = parsed_url
51-
.path_segments()
52-
.ok_or(anyhow!("Error parsing repository remote URL."))?;
53-
54-
let repo_owner = url_paths
55-
.next()
56-
.ok_or(anyhow!(
57-
"Error parsing repository remote URL: Repo owner not found."
58-
))?
59-
.to_string();
60-
let repo_name = url_paths
61-
.next()
62-
.ok_or(anyhow!(
63-
"Error parsing repository remote URL: Repo name not found."
64-
))?
65-
.to_string();
66-
67-
// Only include repositories owned by the organization
68-
if repo_owner == env_vars.gh_org_name {
69-
let collab_role = github::get_collaborator_role(
70-
&client,
71-
&env_vars.gh_org_admin_token,
72-
&repo_owner,
73-
&repo_name,
74-
username,
75-
)
76-
.await?;
77-
78-
// `None` means the user is not a collaborator
79-
if let Some(role) = collab_role.as_deref()
80-
&& (role == "maintain" || role == "admin")
81-
{
82-
deployments.push(Deployment {
83-
name,
84-
repo_url,
85-
repo_owner,
86-
repo_name,
87-
});
88-
}
88+
let project_name = path.file_name().into_string().map_err(|_| anyhow!("Invalid project name"))?;
89+
if let Some(deployment) = check_access(env_vars, username, &project_name).await.ok() {
90+
deployments.push(deployment);
8991
}
9092
}
9193
}
9294

9395
Ok(deployments)
9496
}
97+
98+
#[derive(Deserialize, Serialize)]
99+
/// Settings for a project
100+
pub struct ProjectSettings {
101+
/// Subdirectory which is deployed (relative to the project root)
102+
pub deploy_dir: String,
103+
}
104+
105+
/// Get the project settings (stored in .maint on the top level of the project directory)
106+
pub async fn get_project_settings(env_vars: &EnvVars, project_name: &str) -> Res<ProjectSettings> {
107+
let maint_file_path = format!(
108+
"{}/{}/.maint",
109+
env_vars.deployments_dir.display(),
110+
project_name
111+
);
112+
113+
if let Ok(maint_file_contents) = fs::read_to_string(maint_file_path).await {
114+
Ok(ProjectSettings { deploy_dir: maint_file_contents.trim().into() })
115+
} else {
116+
Ok(ProjectSettings { deploy_dir: ".".into() } )
117+
}
118+
}
119+
120+
/// Get the environment variables for a project
121+
pub async fn get_env(env_vars: &EnvVars, username: &str, project_name: &str) -> Res<Value> {
122+
check_access(env_vars, username, project_name).await?;
123+
124+
let project_settings = get_project_settings(env_vars, project_name).await?;
125+
126+
let env_file_path = PathBuf::from(&env_vars.deployments_dir)
127+
.join(project_name)
128+
.join(&project_settings.deploy_dir)
129+
.join(".env");
130+
131+
let mut map = Map::new();
132+
for item in dotenvy::from_path_iter(env_file_path)? {
133+
let (key, value) = item?;
134+
map.insert(key, Value::String(value));
135+
}
136+
137+
Ok(Value::Object(map))
138+
}

frontend/.env.development

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
VITE_BACKEND_URL=http://localhost:8080
2-
VITE_GH_OAUTH_CLIENT_ID=Ov23liSSsyTFMsm1CT09
2+
VITE_GH_OAUTH_CLIENT_ID=Ov23liN3sRyCG1lEbuyU

0 commit comments

Comments
 (0)