mirror of
https://github.com/moghtech/komodo.git
synced 2026-03-18 06:30:43 -05:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b94fcf3da | ||
|
|
9cf03b8b88 | ||
|
|
a288edcf61 | ||
|
|
89cc18ad37 | ||
|
|
ffa3b671e1 | ||
|
|
f32eeb413b | ||
|
|
b5a5103cfc | ||
|
|
c5697e59f3 | ||
|
|
f030667ff4 | ||
|
|
e9fef5d97c | ||
|
|
f5818ac7ea | ||
|
|
c85ab4110d | ||
|
|
9690ea35b8 | ||
|
|
6300c8011b | ||
|
|
97f582b381 | ||
|
|
5135a9c228 | ||
|
|
b7d1212a82 | ||
|
|
7d9d0a9fc4 | ||
|
|
ed9aef4321 | ||
|
|
0aa638bdf4 | ||
|
|
0ec39d793d |
54
Cargo.lock
generated
54
Cargo.lock
generated
@@ -734,7 +734,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "core"
|
||||
version = "0.2.7"
|
||||
version = "0.2.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -753,7 +753,7 @@ dependencies = [
|
||||
"hmac",
|
||||
"jwt",
|
||||
"monitor_helpers",
|
||||
"monitor_types 0.2.7",
|
||||
"monitor_types 0.2.9",
|
||||
"mungos",
|
||||
"periphery_client",
|
||||
"serde",
|
||||
@@ -987,10 +987,10 @@ checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb"
|
||||
|
||||
[[package]]
|
||||
name = "db_client"
|
||||
version = "0.2.7"
|
||||
version = "0.2.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"monitor_types 0.2.7",
|
||||
"monitor_types 0.2.9",
|
||||
"mungos",
|
||||
]
|
||||
|
||||
@@ -1837,12 +1837,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "monitor_cli"
|
||||
version = "0.2.7"
|
||||
version = "0.2.9"
|
||||
dependencies = [
|
||||
"async_timing_util",
|
||||
"clap",
|
||||
"colored",
|
||||
"monitor_types 0.2.7",
|
||||
"monitor_types 0.2.9",
|
||||
"rand",
|
||||
"run_command",
|
||||
"serde",
|
||||
@@ -1854,12 +1854,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "monitor_client"
|
||||
version = "0.2.7"
|
||||
version = "0.2.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"envy",
|
||||
"futures-util",
|
||||
"monitor_types 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"monitor_types 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@@ -1871,11 +1871,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "monitor_helpers"
|
||||
version = "0.2.7"
|
||||
version = "0.2.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"monitor_types 0.2.7",
|
||||
"monitor_types 0.2.9",
|
||||
"rand",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1884,7 +1884,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "monitor_periphery"
|
||||
version = "0.2.7"
|
||||
version = "0.2.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -1896,11 +1896,12 @@ dependencies = [
|
||||
"envy",
|
||||
"futures",
|
||||
"monitor_helpers",
|
||||
"monitor_types 0.2.7",
|
||||
"monitor_types 0.2.9",
|
||||
"run_command",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"svi",
|
||||
"sysinfo",
|
||||
"tokio",
|
||||
"toml",
|
||||
@@ -1909,7 +1910,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "monitor_types"
|
||||
version = "0.2.7"
|
||||
version = "0.2.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bollard",
|
||||
@@ -1926,9 +1927,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "monitor_types"
|
||||
version = "0.2.7"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1c280239929526ffd057372240260b6a78e7f62bbbc061218a46f607f176f3e"
|
||||
checksum = "7de716e157711aac3646ae9faddf6d48cefc0d3e163cdf1b5a72bb034a353fef"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bollard",
|
||||
@@ -1945,9 +1946,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mungos"
|
||||
version = "0.3.7"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fc5132f76fafd19d773c68520ab427659a7b17484f2b4705f323b60f84e9d6c"
|
||||
checksum = "588d78564faa32532f258d8e9080035b36b10f65289d3bbb6bdbbcccf5cbd3cd"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
@@ -2184,11 +2185,11 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
|
||||
|
||||
[[package]]
|
||||
name = "periphery_client"
|
||||
version = "0.2.7"
|
||||
version = "0.2.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures-util",
|
||||
"monitor_types 0.2.7",
|
||||
"monitor_types 0.2.9",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -2862,6 +2863,15 @@ version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
|
||||
|
||||
[[package]]
|
||||
name = "svi"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec1ee5e6cf961310f3b4ba037f6a3680fc264f9077e0b9f16a0d7cc8d0ade140"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
@@ -3012,9 +3022,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.25.0"
|
||||
version = "1.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af"
|
||||
checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"bytes",
|
||||
@@ -3027,7 +3037,7 @@ dependencies = [
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.42.0",
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
FROM rust:latest as builder
|
||||
WORKDIR /builder
|
||||
|
||||
COPY ./periphery ./periphery
|
||||
|
||||
COPY ./lib/types ./lib/types
|
||||
COPY ./lib/helpers ./lib/helpers
|
||||
|
||||
RUN cd periphery && cargo build --release
|
||||
|
||||
FROM debian:stable-slim
|
||||
|
||||
ARG DEPS_INSTALLER
|
||||
|
||||
COPY ./${DEPS_INSTALLER}.sh ./
|
||||
RUN sh ./${DEPS_INSTALLER}.sh
|
||||
|
||||
COPY --from=builder /builder/periphery/target/release/periphery /usr/local/bin/periphery
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD "periphery"
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "monitor_cli"
|
||||
version = "0.2.7"
|
||||
version = "0.2.9"
|
||||
edition = "2021"
|
||||
authors = ["MoghTech"]
|
||||
description = "monitor cli | tools to setup monitor system"
|
||||
|
||||
@@ -62,6 +62,7 @@ pub fn gen_core_config(sub_matches: &ArgMatches) {
|
||||
.map(|p| p.to_owned());
|
||||
|
||||
let config = CoreConfig {
|
||||
title: String::from("monitor"),
|
||||
host,
|
||||
port,
|
||||
jwt_valid_for,
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# optional. this will be the document title on the web page (shows up as text in the browser tab). default is 'monitor'
|
||||
title = "monitor"
|
||||
|
||||
# this should be the url used to access monitor in browser, potentially behind DNS, eg https://monitor.mogh.tech or http://12.34.56.78:9000
|
||||
host = "https://monitor.mogh.tech"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "core"
|
||||
version = "0.2.7"
|
||||
version = "0.2.9"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@@ -11,7 +11,7 @@ types = { package = "monitor_types", path = "../lib/types" }
|
||||
db = { package = "db_client", path = "../lib/db_client" }
|
||||
periphery = { package = "periphery_client", path = "../lib/periphery_client" }
|
||||
axum_oauth2 = { path = "../lib/axum_oauth2" }
|
||||
tokio = { version = "1.25", features = ["full"] }
|
||||
tokio = { version = "1.26", features = ["full"] }
|
||||
tokio-tungstenite = { version = "0.18", features=["native-tls"] }
|
||||
tokio-util = "0.7"
|
||||
axum = { version = "0.6", features = ["ws", "json"] }
|
||||
@@ -19,7 +19,7 @@ axum-extra = { version = "0.5.0", features = ["spa"] }
|
||||
tower = { version = "0.4", features = ["full"] }
|
||||
tower-http = { version = "0.4.0", features = ["cors"] }
|
||||
slack = { package = "slack_client_rs", version = "0.0.8" }
|
||||
mungos = "0.3.3"
|
||||
mungos = "0.3.8"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1.0"
|
||||
|
||||
@@ -489,10 +489,10 @@ impl State {
|
||||
if let Some(version) = update.version {
|
||||
Ok(version.to_string())
|
||||
} else {
|
||||
Ok("latest".to_string())
|
||||
Ok("unknown".to_string())
|
||||
}
|
||||
} else {
|
||||
Ok("latest".to_string())
|
||||
Ok("unknown".to_string())
|
||||
}
|
||||
} else {
|
||||
let split = deployment
|
||||
@@ -503,7 +503,7 @@ impl State {
|
||||
if let Some(version) = split.get(1) {
|
||||
Ok(version.to_string())
|
||||
} else {
|
||||
Ok("latest".to_string())
|
||||
Ok("unknown".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@ use typeshare::typeshare;
|
||||
|
||||
use crate::{
|
||||
auth::{auth_request, JwtExtension, RequestUser, RequestUserExtension},
|
||||
response,
|
||||
state::{State, StateExtension},
|
||||
ResponseResult,
|
||||
};
|
||||
|
||||
pub mod build;
|
||||
@@ -35,22 +37,33 @@ struct UpdateDescriptionBody {
|
||||
description: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct UserId {
|
||||
id: String,
|
||||
}
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
"/user",
|
||||
get(|jwt, req| async { get_user(jwt, req).await.map_err(handle_anyhow_error) }),
|
||||
"/title",
|
||||
get(|state: StateExtension| async move { state.config.title.clone() }),
|
||||
)
|
||||
.route("/user", get(get_request_user))
|
||||
.nest("/listener", github_listener::router())
|
||||
.nest(
|
||||
"/",
|
||||
Router::new()
|
||||
.route("/user/:id", get(get_user_at_id))
|
||||
.route(
|
||||
"/username/:id",
|
||||
get(|state, user_id| async {
|
||||
get_username(state, user_id)
|
||||
get(|state: StateExtension, Path(UserId { id })| async move {
|
||||
let user = state
|
||||
.db
|
||||
.get_user(&id)
|
||||
.await
|
||||
.map_err(handle_anyhow_error)
|
||||
.context("failed to find user at id")
|
||||
.map_err(handle_anyhow_error)?;
|
||||
response!(Json(user.username))
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
@@ -90,8 +103,11 @@ pub fn router() -> Router {
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_user(Extension(jwt): JwtExtension, req: Request<Body>) -> anyhow::Result<Json<User>> {
|
||||
let mut user = jwt.authenticate(&req).await?;
|
||||
async fn get_request_user(
|
||||
Extension(jwt): JwtExtension,
|
||||
req: Request<Body>,
|
||||
) -> ResponseResult<Json<User>> {
|
||||
let mut user = jwt.authenticate(&req).await.map_err(handle_anyhow_error)?;
|
||||
user.password = None;
|
||||
for secret in &mut user.secrets {
|
||||
secret.hash = String::new();
|
||||
@@ -99,23 +115,10 @@ async fn get_user(Extension(jwt): JwtExtension, req: Request<Body>) -> anyhow::R
|
||||
Ok(Json(user))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct UserId {
|
||||
id: String,
|
||||
}
|
||||
|
||||
async fn get_username(
|
||||
state: StateExtension,
|
||||
Path(UserId { id }): Path<UserId>,
|
||||
) -> anyhow::Result<String> {
|
||||
let user = state.db.get_user(&id).await?;
|
||||
Ok(user.username)
|
||||
}
|
||||
|
||||
async fn get_users(
|
||||
state: StateExtension,
|
||||
user: RequestUserExtension,
|
||||
) -> Result<Json<Vec<User>>, (StatusCode, String)> {
|
||||
) -> ResponseResult<Json<Vec<User>>> {
|
||||
if user.is_admin {
|
||||
let users = state
|
||||
.db
|
||||
@@ -137,8 +140,33 @@ async fn get_users(
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_user_at_id(
|
||||
state: StateExtension,
|
||||
Path(UserId { id }): Path<UserId>,
|
||||
user: RequestUserExtension,
|
||||
) -> ResponseResult<Json<User>> {
|
||||
if user.is_admin {
|
||||
let mut user = state
|
||||
.db
|
||||
.users
|
||||
.find_one_by_id(&id)
|
||||
.await
|
||||
.context("failed at query to get user from mongo")
|
||||
.map_err(handle_anyhow_error)?
|
||||
.ok_or(anyhow!(""))
|
||||
.map_err(handle_anyhow_error)?;
|
||||
user.password = None;
|
||||
for secret in &mut user.secrets {
|
||||
secret.hash = String::new();
|
||||
}
|
||||
Ok(Json(user))
|
||||
} else {
|
||||
Err((StatusCode::UNAUTHORIZED, "user is not admin".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
// need to run requested actions in here to prevent them being dropped mid action when user disconnects prematurely
|
||||
pub async fn spawn_request_action<A>(action: A) -> Result<A::Output, (StatusCode, String)>
|
||||
pub async fn spawn_request_action<A>(action: A) -> ResponseResult<A::Output>
|
||||
where
|
||||
A: Future + Send + 'static,
|
||||
A::Output: Send + 'static,
|
||||
|
||||
@@ -284,7 +284,7 @@ async fn modify_user_create_server_permissions(
|
||||
"user does not have permissions for this action (not admin)"
|
||||
));
|
||||
}
|
||||
let user = state
|
||||
let target_user = state
|
||||
.db
|
||||
.users
|
||||
.find_one_by_id(&user_id)
|
||||
@@ -312,7 +312,7 @@ async fn modify_user_create_server_permissions(
|
||||
"modify user create server permissions",
|
||||
format!(
|
||||
"{update_type} create server permissions for {} (id: {})",
|
||||
user.username, user.id
|
||||
target_user.username, target_user.id
|
||||
),
|
||||
)],
|
||||
start_ts: ts.clone(),
|
||||
@@ -339,7 +339,7 @@ async fn modify_user_create_build_permissions(
|
||||
"user does not have permissions for this action (not admin)"
|
||||
));
|
||||
}
|
||||
let user = state
|
||||
let target_user = state
|
||||
.db
|
||||
.users
|
||||
.find_one_by_id(&user_id)
|
||||
@@ -367,7 +367,7 @@ async fn modify_user_create_build_permissions(
|
||||
"modify user create build permissions",
|
||||
format!(
|
||||
"{update_type} create build permissions for {} (id: {})",
|
||||
user.username, user.id
|
||||
target_user.username, target_user.id
|
||||
),
|
||||
)],
|
||||
start_ts: ts.clone(),
|
||||
|
||||
@@ -339,6 +339,20 @@ pub fn router() -> Router {
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/:id/secrets",
|
||||
get(
|
||||
|state: StateExtension,
|
||||
user: RequestUserExtension,
|
||||
Path(ServerId { id })| async move {
|
||||
let vars = state
|
||||
.get_available_secrets(&id, &user)
|
||||
.await
|
||||
.map_err(handle_anyhow_error)?;
|
||||
response!(Json(vars))
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/:id/action_state",
|
||||
get(
|
||||
@@ -628,6 +642,18 @@ impl State {
|
||||
Ok(docker_accounts)
|
||||
}
|
||||
|
||||
async fn get_available_secrets(
|
||||
&self,
|
||||
id: &str,
|
||||
user: &RequestUser,
|
||||
) -> anyhow::Result<Vec<String>> {
|
||||
let server = self
|
||||
.get_server_check_permissions(id, user, PermissionLevel::Read)
|
||||
.await?;
|
||||
let vars = self.periphery.get_available_secrets(&server).await?;
|
||||
Ok(vars)
|
||||
}
|
||||
|
||||
async fn get_server_action_states(
|
||||
&self,
|
||||
id: String,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use ::helpers::get_socket_addr;
|
||||
use auth::JwtClient;
|
||||
use axum::Router;
|
||||
use axum::{http::StatusCode, Router};
|
||||
use state::State;
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
|
||||
@@ -16,8 +16,10 @@ mod monitoring;
|
||||
mod state;
|
||||
mod ws;
|
||||
|
||||
type ResponseResult<T> = Result<T, (StatusCode, String)>;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let (config, spa_router) = config::load();
|
||||
|
||||
println!("starting monitor core on port {}...", config.port);
|
||||
@@ -40,6 +42,7 @@ async fn main() {
|
||||
|
||||
axum::Server::bind(&get_socket_addr(config.port))
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.expect("monitor core axum server crashed");
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -412,8 +412,12 @@ impl State {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let servers = servers.unwrap();
|
||||
if servers.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let mut blocks = vec![Block::header("INFO | daily update"), Block::divider()];
|
||||
for (server, stats) in servers.unwrap() {
|
||||
for (server, stats) in servers {
|
||||
let region = if let Some(region) = &server.region {
|
||||
format!(" | {region}")
|
||||
} else {
|
||||
|
||||
@@ -2,7 +2,6 @@ use std::sync::Arc;
|
||||
|
||||
use axum::{
|
||||
extract::{ws::Message as AxumMessage, Path, Query, WebSocketUpgrade},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
@@ -13,7 +12,7 @@ use tokio_tungstenite::tungstenite::Message;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use types::{traits::Permissioned, PermissionLevel, SystemStatsQuery};
|
||||
|
||||
use crate::{auth::JwtExtension, state::StateExtension};
|
||||
use crate::{auth::JwtExtension, state::StateExtension, ResponseResult};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ServerId {
|
||||
@@ -26,7 +25,7 @@ pub async fn ws_handler(
|
||||
path: Path<ServerId>,
|
||||
query: Query<SystemStatsQuery>,
|
||||
ws: WebSocketUpgrade,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
) -> ResponseResult<impl IntoResponse> {
|
||||
let server = state
|
||||
.db
|
||||
.get_server(&path.id)
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@solidjs/router": "^0.6.0",
|
||||
"@tanstack/solid-query": "^4.26.0",
|
||||
"axios": "^1.2.1",
|
||||
"js-file-download": "^0.4.12",
|
||||
"lightweight-charts": "^3.8.0",
|
||||
|
||||
@@ -8,8 +8,9 @@ const Deployment = lazy(() => import("./components/deployment/Deployment"));
|
||||
const Server = lazy(() => import("./components/server/Server"));
|
||||
const Build = lazy(() => import("./components/build/Build"));
|
||||
const Users = lazy(() => import("./components/users/Users"));
|
||||
const User = lazy(() => import("./components/users/User"));
|
||||
const Stats = lazy(() => import("./components/stats/Stats"));
|
||||
const Account = lazy(() => import("./components/Account"));
|
||||
const Account = lazy(() => import("./components/account/Account"));
|
||||
|
||||
const App: Component = () => {
|
||||
const { user } = useUser();
|
||||
@@ -25,6 +26,7 @@ const App: Component = () => {
|
||||
<Route path="/account" component={Account} />
|
||||
<Show when={user().admin}>
|
||||
<Route path="/users" component={Users} />
|
||||
<Route path="/user/:id" component={User} />
|
||||
</Show>
|
||||
</Routes>
|
||||
</div>
|
||||
|
||||
@@ -3,8 +3,6 @@ import { client, pushNotification } from "..";
|
||||
import { useAppState } from "../state/StateProvider";
|
||||
import { UpdateTarget } from "../types";
|
||||
import { useToggle } from "../util/hooks";
|
||||
import ConfirmButton from "./shared/ConfirmButton";
|
||||
import Flex from "./shared/layout/Flex";
|
||||
import Grid from "./shared/layout/Grid";
|
||||
import Loading from "./shared/loading/Loading";
|
||||
import CenterMenu from "./shared/menu/CenterMenu";
|
||||
@@ -19,7 +17,8 @@ const Description: Component<{
|
||||
const [show, toggleShow] = useToggle();
|
||||
const description = () => {
|
||||
if (p.description) {
|
||||
return p.description;
|
||||
let [description] = p.description.split("\n");
|
||||
return description;
|
||||
} else {
|
||||
return "add a description";
|
||||
}
|
||||
@@ -110,8 +109,7 @@ const DescriptionMenu: Component<{
|
||||
placeholder="add a description"
|
||||
value={desc()}
|
||||
onEdit={setDesc}
|
||||
onEnter={update_description}
|
||||
style={{ width: "700px", "max-width": "90vw", padding: "1rem" }}
|
||||
style={{ width: "900px", "max-width": "90vw", height: "70vh", padding: "1rem" }}
|
||||
disabled={!p.userCanUpdate}
|
||||
/>
|
||||
<Show when={p.userCanUpdate}>
|
||||
|
||||
41
frontend/src/components/account/Account.tsx
Normal file
41
frontend/src/components/account/Account.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Component, Show } from "solid-js";
|
||||
import { useUser } from "../../state/UserProvider";
|
||||
import { readableMonitorTimestamp, readableUserType } from "../../util/helpers";
|
||||
import Flex from "../shared/layout/Flex";
|
||||
import Resources from "./Resources";
|
||||
import Secrets from "./Secrets";
|
||||
|
||||
const Account: Component<{}> = (p) => {
|
||||
const { user, username } = useUser();
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
class="card shadow"
|
||||
style={{ width: "100%", "box-sizing": "border-box" }}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<h1>{username()}</h1>
|
||||
<Flex>
|
||||
<Show when={user().admin}>
|
||||
<div class="dimmed">admin</div>
|
||||
</Show>
|
||||
<Flex gap="0.5rem">
|
||||
<div class="dimmed">type:</div>
|
||||
<div>{user() ? readableUserType(user()!) : "unknown"}</div>
|
||||
</Flex>
|
||||
<Flex gap="0.5rem">
|
||||
<div class="dimmed">created:</div>
|
||||
<div>{readableMonitorTimestamp(user().created_at!)}</div>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Secrets />
|
||||
<Show when={!user().admin}>
|
||||
<Resources />
|
||||
</Show>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Account;
|
||||
124
frontend/src/components/account/Resources.tsx
Normal file
124
frontend/src/components/account/Resources.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import { A } from "@solidjs/router";
|
||||
import { Component, createMemo, createSignal, For } from "solid-js";
|
||||
import { useAppDimensions } from "../../state/DimensionProvider";
|
||||
import { useAppState } from "../../state/StateProvider";
|
||||
import { useUser } from "../../state/UserProvider";
|
||||
import { PermissionLevel } from "../../types";
|
||||
import { getId } from "../../util/helpers";
|
||||
import Flex from "../shared/layout/Flex";
|
||||
import Grid from "../shared/layout/Grid";
|
||||
|
||||
const Resources: Component<{}> = (p) => {
|
||||
const { user, user_id } = useUser();
|
||||
const { isMobile } = useAppDimensions();
|
||||
const { builds, deployments, servers } = useAppState();
|
||||
const [search, setSearch] = createSignal("");
|
||||
const _servers = createMemo(() => {
|
||||
return servers.filterArray((s) => {
|
||||
if (!s.server.name.includes(search())) return false;
|
||||
const p = s.server.permissions?.[user_id()];
|
||||
return p ? p !== PermissionLevel.None : false;
|
||||
});
|
||||
});
|
||||
const _deployments = createMemo(() => {
|
||||
return deployments.filterArray((d) => {
|
||||
if (!d.deployment.name.includes(search())) return false;
|
||||
const p = d.deployment.permissions?.[user_id()];
|
||||
return p ? p !== PermissionLevel.None : false;
|
||||
});
|
||||
});
|
||||
const _builds = createMemo(() => {
|
||||
return builds.filterArray((b) => {
|
||||
if (!b.name.includes(search())) return false;
|
||||
const p = b.permissions?.[user_id()];
|
||||
return p ? p !== PermissionLevel.None : false;
|
||||
});
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<Grid
|
||||
class="card shadow"
|
||||
style={{ width: "100%", "box-sizing": "border-box" }}
|
||||
>
|
||||
<h1>servers</h1>
|
||||
<Grid gridTemplateColumns={isMobile() ? undefined : "1fr 1fr"}>
|
||||
<For each={_servers()}>
|
||||
{(item) => (
|
||||
<A
|
||||
class="card light shadow"
|
||||
href={`/server/${getId(item.server)}`}
|
||||
style={{
|
||||
"justify-content": "space-between",
|
||||
width: "100%",
|
||||
"box-sizing": "border-box",
|
||||
}}
|
||||
>
|
||||
<Grid gap="0.25rem">
|
||||
<h2>{item.server.name}</h2>
|
||||
<div class="dimmed">{item.server.region || "unknown region"}</div>
|
||||
</Grid>
|
||||
<div>{item.server.permissions?.[user_id()] || "none"}</div>
|
||||
</A>
|
||||
)}
|
||||
</For>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid
|
||||
class="card shadow"
|
||||
style={{ width: "100%", "box-sizing": "border-box" }}
|
||||
>
|
||||
<h1>deployments</h1>
|
||||
<Grid gridTemplateColumns={isMobile() ? undefined : "1fr 1fr"}>
|
||||
<For each={_deployments()}>
|
||||
{(item) => (
|
||||
<A
|
||||
href={`/deployment/${getId(item.deployment)}`}
|
||||
class="card light shadow"
|
||||
style={{
|
||||
"justify-content": "space-between",
|
||||
width: "100%",
|
||||
"box-sizing": "border-box",
|
||||
}}
|
||||
>
|
||||
<Grid gap="0.25rem">
|
||||
<h2>{item.deployment.name}</h2>
|
||||
<div class="dimmed">
|
||||
{servers.get(item.deployment.server_id)?.server.name ||
|
||||
"unknown"}
|
||||
</div>
|
||||
</Grid>
|
||||
<div>{item.deployment.permissions?.[user_id()] || "none"}</div>
|
||||
</A>
|
||||
)}
|
||||
</For>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid
|
||||
class="card shadow"
|
||||
style={{ width: "100%", "box-sizing": "border-box" }}
|
||||
>
|
||||
<h1>builds</h1>
|
||||
<Grid gridTemplateColumns={isMobile() ? undefined : "1fr 1fr"}>
|
||||
<For each={_builds()}>
|
||||
{(item) => (
|
||||
<A
|
||||
href={`/build/${getId(item)}`}
|
||||
class="card light shadow"
|
||||
style={{
|
||||
"justify-content": "space-between",
|
||||
width: "100%",
|
||||
"box-sizing": "border-box",
|
||||
}}
|
||||
>
|
||||
<h2>{item.name}</h2>
|
||||
<div>{item.permissions?.[user_id()] || "none"}</div>
|
||||
</A>
|
||||
)}
|
||||
</For>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Resources;
|
||||
@@ -1,79 +1,81 @@
|
||||
import { Component, For, Match, Show, Switch } from "solid-js";
|
||||
import { Component, For, Show } from "solid-js";
|
||||
import { createStore } from "solid-js/store";
|
||||
import { client, pushNotification } from "..";
|
||||
import { useUser } from "../state/UserProvider";
|
||||
import { copyToClipboard, readableMonitorTimestamp } from "../util/helpers";
|
||||
import { useToggle } from "../util/hooks";
|
||||
import ConfirmButton from "./shared/ConfirmButton";
|
||||
import Icon from "./shared/Icon";
|
||||
import Input from "./shared/Input";
|
||||
import Flex from "./shared/layout/Flex";
|
||||
import Grid from "./shared/layout/Grid";
|
||||
import Loading from "./shared/loading/Loading";
|
||||
import CenterMenu from "./shared/menu/CenterMenu";
|
||||
import Selector from "./shared/menu/Selector";
|
||||
import { client, pushNotification } from "../..";
|
||||
import { useUser } from "../../state/UserProvider";
|
||||
import { copyToClipboard, readableMonitorTimestamp } from "../../util/helpers";
|
||||
import { useToggle } from "../../util/hooks";
|
||||
import ConfirmButton from "../shared/ConfirmButton";
|
||||
import Icon from "../shared/Icon";
|
||||
import Input from "../shared/Input";
|
||||
import Flex from "../shared/layout/Flex";
|
||||
import Grid from "../shared/layout/Grid";
|
||||
import Loading from "../shared/loading/Loading";
|
||||
import CenterMenu from "../shared/menu/CenterMenu";
|
||||
import Selector from "../shared/menu/Selector";
|
||||
|
||||
const Account: Component<{}> = (p) => {
|
||||
const { user, reloadUser } = useUser();
|
||||
const Secrets: Component<{}> = (p) => {
|
||||
const { user, reloadUser } = useUser();
|
||||
const [showCreate, toggleShowCreate] = useToggle();
|
||||
return (
|
||||
<>
|
||||
<Grid
|
||||
class="card shadow"
|
||||
style={{ width: "100%", "box-sizing": "border-box" }}
|
||||
>
|
||||
<Flex justifyContent="space-between">
|
||||
<h1>api secrets</h1>
|
||||
<CenterMenu
|
||||
show={showCreate}
|
||||
toggleShow={toggleShowCreate}
|
||||
targetClass="green"
|
||||
title="create secret"
|
||||
target={<Icon type="plus" />}
|
||||
content={() => <CreateNewMenu />}
|
||||
position="center"
|
||||
/>
|
||||
</Flex>
|
||||
<For each={user().secrets}>
|
||||
{(secret) => (
|
||||
<Flex
|
||||
class="card dark shadow wrap"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<h2>{secret.name}</h2>
|
||||
<Flex alignItems="center">
|
||||
<Flex gap="0.25rem">
|
||||
<div style={{ opacity: 0.7 }}>created:</div>
|
||||
<div>{readableMonitorTimestamp(secret.created_at)}</div>
|
||||
</Flex>
|
||||
<Flex gap="0.25rem">
|
||||
<div style={{ opacity: 0.7 }}>expires:</div>
|
||||
<div>{secret.expires ? readableMonitorTimestamp(secret.expires) : "never"}</div>
|
||||
</Flex>
|
||||
<ConfirmButton
|
||||
class="red"
|
||||
onConfirm={() =>
|
||||
client.delete_api_secret(secret.name).then(reloadUser)
|
||||
}
|
||||
>
|
||||
<Icon type="trash" />
|
||||
</ConfirmButton>
|
||||
return (
|
||||
<Grid
|
||||
class="card shadow"
|
||||
style={{ width: "100%", "box-sizing": "border-box" }}
|
||||
>
|
||||
<Flex justifyContent="space-between">
|
||||
<h1>api secrets</h1>
|
||||
<CenterMenu
|
||||
show={showCreate}
|
||||
toggleShow={toggleShowCreate}
|
||||
targetClass="green"
|
||||
title="create secret"
|
||||
target={<Icon type="plus" />}
|
||||
content={() => <CreateNewSecretMenu />}
|
||||
position="center"
|
||||
/>
|
||||
</Flex>
|
||||
<For each={user().secrets}>
|
||||
{(secret) => (
|
||||
<Flex
|
||||
class="card light shadow wrap"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<h2>{secret.name}</h2>
|
||||
<Flex alignItems="center">
|
||||
<Flex gap="0.25rem">
|
||||
<div style={{ opacity: 0.7 }}>created:</div>
|
||||
<div>{readableMonitorTimestamp(secret.created_at)}</div>
|
||||
</Flex>
|
||||
<Flex gap="0.25rem">
|
||||
<div style={{ opacity: 0.7 }}>expires:</div>
|
||||
<div>
|
||||
{secret.expires
|
||||
? readableMonitorTimestamp(secret.expires)
|
||||
: "never"}
|
||||
</div>
|
||||
</Flex>
|
||||
<ConfirmButton
|
||||
class="red"
|
||||
onConfirm={() =>
|
||||
client.delete_api_secret(secret.name).then(reloadUser)
|
||||
}
|
||||
>
|
||||
<Icon type="trash" />
|
||||
</ConfirmButton>
|
||||
</Flex>
|
||||
)}
|
||||
</For>
|
||||
</Grid>
|
||||
</>
|
||||
</Flex>
|
||||
)}
|
||||
</For>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default Account;
|
||||
export default Secrets;
|
||||
|
||||
const EXPIRE_LENGTHS = ["30 days", "90 days", "1 year", "never"] as const;
|
||||
type ExpireLength = typeof EXPIRE_LENGTHS[number];
|
||||
|
||||
const CreateNewMenu = () => {
|
||||
const CreateNewSecretMenu = () => {
|
||||
const { reloadUser } = useUser();
|
||||
const [info, setInfo] = createStore<{
|
||||
name: string;
|
||||
@@ -167,4 +169,4 @@ function createExpires(length: ExpireLength) {
|
||||
const add_days = length === "30 days" ? 30 : length === "90 days" ? 90 : 365;
|
||||
const add_ms = add_days * 24 * 60 * 60 * 1000;
|
||||
return new Date(Date.now() + add_ms).toISOString();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
import { Component, createEffect, createSignal, Show } from "solid-js";
|
||||
import { Component, createEffect, createSignal, For, Show } from "solid-js";
|
||||
import { client } from "../../../..";
|
||||
import { useAppState } from "../../../../state/StateProvider";
|
||||
import { ServerStatus } from "../../../../types";
|
||||
import {
|
||||
parseDotEnvToEnvVars,
|
||||
parseEnvVarseToDotEnv,
|
||||
} from "../../../../util/helpers";
|
||||
import { useToggle } from "../../../../util/hooks";
|
||||
import Flex from "../../../shared/layout/Flex";
|
||||
import Grid from "../../../shared/layout/Grid";
|
||||
import CenterMenu from "../../../shared/menu/CenterMenu";
|
||||
import TextArea from "../../../shared/TextArea";
|
||||
import { useConfig } from "../Provider";
|
||||
@@ -36,9 +40,10 @@ const BuildArgs: Component<{}> = (p) => {
|
||||
};
|
||||
|
||||
const EditBuildArgs: Component<{}> = (p) => {
|
||||
const { aws_builder_config } = useAppState();
|
||||
const [show, toggle] = useToggle();
|
||||
const [buildArgs, setBuildArgs] = createSignal("");
|
||||
const { build, setBuild } = useConfig();
|
||||
const { build, setBuild, server } = useConfig();
|
||||
createEffect(() => {
|
||||
setBuildArgs(
|
||||
parseEnvVarseToDotEnv(
|
||||
@@ -56,6 +61,27 @@ const EditBuildArgs: Component<{}> = (p) => {
|
||||
}
|
||||
toggle();
|
||||
};
|
||||
const [peripherySecrets, setPeripherySecrets] =
|
||||
createSignal<string[]>();
|
||||
createEffect(() => {
|
||||
if (server()?.status === ServerStatus.Ok) {
|
||||
client
|
||||
.get_server_available_secrets(build.server_id!)
|
||||
.then(setPeripherySecrets);
|
||||
}
|
||||
});
|
||||
const secrets = () => {
|
||||
if (build.server_id) {
|
||||
return peripherySecrets() || [];
|
||||
} else if (build.aws_config) {
|
||||
const ami_name =
|
||||
build.aws_config?.ami_name || aws_builder_config()?.default_ami_name;
|
||||
return ami_name
|
||||
? aws_builder_config()?.available_ami_accounts![ami_name].secrets || []
|
||||
: [];
|
||||
} else return [];
|
||||
};
|
||||
let ref: HTMLTextAreaElement;
|
||||
return (
|
||||
<CenterMenu
|
||||
show={show}
|
||||
@@ -69,19 +95,44 @@ const EditBuildArgs: Component<{}> = (p) => {
|
||||
</button>
|
||||
)}
|
||||
content={() => (
|
||||
<TextArea
|
||||
class="scroller"
|
||||
placeholder="VARIABLE=value #example"
|
||||
value={buildArgs()}
|
||||
onEdit={setBuildArgs}
|
||||
style={{
|
||||
width: "1000px",
|
||||
"max-width": "90vw",
|
||||
height: "80vh",
|
||||
padding: "1rem",
|
||||
}}
|
||||
spellcheck={false}
|
||||
/>
|
||||
<Grid>
|
||||
<Show when={secrets()?.length || 0 > 0}>
|
||||
<Flex class="wrap" justifyContent="flex-end" alignItems="center">
|
||||
<h2 class="dimmed">secrets:</h2>
|
||||
<For each={secrets()}>
|
||||
{(secret) => (
|
||||
<button
|
||||
class="blue"
|
||||
onClick={() =>
|
||||
setBuildArgs(
|
||||
(args) =>
|
||||
args.slice(0, ref.selectionStart) +
|
||||
`[[${secret}]]` +
|
||||
args.slice(ref.selectionStart, undefined)
|
||||
)
|
||||
}
|
||||
>
|
||||
{secret}
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</Flex>
|
||||
</Show>
|
||||
<TextArea
|
||||
ref={ref! as any}
|
||||
class="scroller"
|
||||
placeholder="VARIABLE=value #example"
|
||||
value={buildArgs()}
|
||||
onEdit={setBuildArgs}
|
||||
style={{
|
||||
width: "1000px",
|
||||
"max-width": "90vw",
|
||||
height: "80vh",
|
||||
padding: "1rem",
|
||||
}}
|
||||
spellcheck={false}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -41,24 +41,36 @@ const Header: Component<{}> = (p) => {
|
||||
const [deployed_version] = createResource(() =>
|
||||
client.get_deployment_deployed_version(params.id)
|
||||
);
|
||||
const image = () => {
|
||||
const derived_image = () => {
|
||||
if (deployment().deployment.build_id) {
|
||||
const build = builds.get(deployment().deployment.build_id!)!;
|
||||
if (deployment().state === DockerContainerState.NotDeployed) {
|
||||
const version = deployment().deployment.build_version
|
||||
? readableVersion(deployment().deployment.build_version!).replaceAll(
|
||||
"v",
|
||||
""
|
||||
)
|
||||
: "latest";
|
||||
return `${build.name}:${version}`;
|
||||
} else {
|
||||
return deployed_version() && `${build.name}:${deployed_version()}`;
|
||||
}
|
||||
const build = builds.get(deployment().deployment.build_id!);
|
||||
if (build === undefined) return "unknown";
|
||||
const version =
|
||||
deployment().state === DockerContainerState.NotDeployed
|
||||
? deployment().deployment.build_version
|
||||
? readableVersion(
|
||||
deployment().deployment.build_version!
|
||||
).replaceAll("v", "")
|
||||
: "latest"
|
||||
: deployed_version() || "unknown";
|
||||
return `${build.name}:${version}`;
|
||||
} else {
|
||||
return deployment().deployment.docker_run_args.image || "unknown";
|
||||
}
|
||||
};
|
||||
const image = () => {
|
||||
if (deployment().state === DockerContainerState.NotDeployed) {
|
||||
return derived_image();
|
||||
} else if (deployment().container?.image) {
|
||||
if (deployment().container!.image.includes("sha256:")) {
|
||||
return derived_image();
|
||||
}
|
||||
let [account, image] = deployment().container!.image.split("/");
|
||||
return image ? image : account;
|
||||
} else {
|
||||
return "unknown";
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Grid
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Component, createEffect, createSignal, Show } from "solid-js";
|
||||
import { Component, createEffect, createSignal, For, Show } from "solid-js";
|
||||
import { client } from "../../../../..";
|
||||
import { ServerStatus } from "../../../../../types";
|
||||
import {
|
||||
combineClasses,
|
||||
parseDotEnvToEnvVars,
|
||||
@@ -38,7 +40,7 @@ const Env: Component<{}> = (p) => {
|
||||
const EditDotEnv: Component<{}> = (p) => {
|
||||
const [show, toggle] = useToggle();
|
||||
const [dotenv, setDotEnv] = createSignal("");
|
||||
const { deployment, setDeployment } = useConfig();
|
||||
const { deployment, setDeployment, server } = useConfig();
|
||||
createEffect(() => {
|
||||
setDotEnv(
|
||||
parseEnvVarseToDotEnv(
|
||||
@@ -56,6 +58,15 @@ const EditDotEnv: Component<{}> = (p) => {
|
||||
}
|
||||
toggle();
|
||||
};
|
||||
const [secrets, setSecrets] = createSignal<string[]>();
|
||||
createEffect(() => {
|
||||
if (server()?.status === ServerStatus.Ok) {
|
||||
client
|
||||
.get_server_available_secrets(deployment.server_id)
|
||||
.then(setSecrets);
|
||||
}
|
||||
});
|
||||
let ref: HTMLTextAreaElement;
|
||||
return (
|
||||
<CenterMenu
|
||||
show={show}
|
||||
@@ -69,19 +80,44 @@ const EditDotEnv: Component<{}> = (p) => {
|
||||
</button>
|
||||
)}
|
||||
content={() => (
|
||||
<TextArea
|
||||
class="scroller"
|
||||
placeholder="VARIABLE=value #example"
|
||||
value={dotenv()}
|
||||
onEdit={setDotEnv}
|
||||
style={{
|
||||
width: "1000px",
|
||||
"max-width": "90vw",
|
||||
height: "80vh",
|
||||
padding: "1rem",
|
||||
}}
|
||||
spellcheck={false}
|
||||
/>
|
||||
<Grid>
|
||||
<Show when={secrets()?.length || 0 > 0}>
|
||||
<Flex class="wrap" justifyContent="flex-end" alignItems="center">
|
||||
<h2 class="dimmed">secrets:</h2>
|
||||
<For each={secrets()}>
|
||||
{(secret) => (
|
||||
<button
|
||||
class="blue"
|
||||
onClick={() =>
|
||||
setDotEnv(
|
||||
(env) =>
|
||||
env.slice(0, ref.selectionStart) +
|
||||
`[[${secret}]]` +
|
||||
env.slice(ref.selectionStart, undefined)
|
||||
)
|
||||
}
|
||||
>
|
||||
{secret}
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</Flex>
|
||||
</Show>
|
||||
<TextArea
|
||||
ref={ref! as any}
|
||||
class="scroller"
|
||||
placeholder="VARIABLE=value #example"
|
||||
value={dotenv()}
|
||||
onEdit={setDotEnv}
|
||||
style={{
|
||||
width: "1000px",
|
||||
"max-width": "90vw",
|
||||
height: "80vh",
|
||||
padding: "1rem",
|
||||
}}
|
||||
spellcheck={false}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -58,6 +58,7 @@ const Builds: Component<{}> = (p) => {
|
||||
style={{ width: "100%", padding: "0.5rem" }}
|
||||
/>
|
||||
<Selector
|
||||
label={<div class="dimmed">sort by:</div>}
|
||||
selected={sort()}
|
||||
items={TREE_SORTS as any as string[]}
|
||||
onSelect={(mode) => setSort(mode as TreeSortType)}
|
||||
|
||||
@@ -51,6 +51,7 @@ const Groups: Component<{}> = (p) => {
|
||||
style={{ width: "100%", padding: "0.5rem" }}
|
||||
/>
|
||||
<Selector
|
||||
label={<div class="dimmed">sort by:</div>}
|
||||
selected={sort()}
|
||||
items={TREE_SORTS as any as string[]}
|
||||
onSelect={(mode) => setSort(mode as TreeSortType)}
|
||||
|
||||
@@ -2,14 +2,14 @@ import { ParentComponent, createContext, useContext } from "solid-js";
|
||||
import { useAppState } from "../../../state/StateProvider";
|
||||
import { useLocalStorage } from "../../../util/hooks";
|
||||
|
||||
export const TREE_SORTS = ["name", "created"] as const;
|
||||
export const TREE_SORTS = ["name", "created at"] as const;
|
||||
export type TreeSortType = typeof TREE_SORTS[number];
|
||||
|
||||
const value = () => {
|
||||
const { servers, groups, builds } = useAppState();
|
||||
const [sort, setSort] = useLocalStorage<TreeSortType>(
|
||||
TREE_SORTS[0],
|
||||
"home-sort-v1"
|
||||
"home-sort-v2"
|
||||
);
|
||||
const server_sorter = () => {
|
||||
if (!servers.loaded()) return () => 0;
|
||||
|
||||
@@ -42,7 +42,8 @@ const Servers: Component<{ serverIDs: string[]; showAdd?: boolean }> = (p) => {
|
||||
onEdit={setServerFilter}
|
||||
style={{ width: "100%", padding: "0.5rem" }}
|
||||
/>
|
||||
<Selector
|
||||
<Selector
|
||||
label={<div class="dimmed">sort by:</div>}
|
||||
selected={sort()}
|
||||
items={TREE_SORTS as any as string[]}
|
||||
onSelect={(mode) => setSort(mode as TreeSortType)}
|
||||
|
||||
@@ -1,36 +1,46 @@
|
||||
import { A } from "@solidjs/router";
|
||||
import { Component, Show } from "solid-js";
|
||||
import { Component, createResource, Show } from "solid-js";
|
||||
import { client } from "../..";
|
||||
import { useAppState } from "../../state/StateProvider";
|
||||
import { DockerContainerState } from "../../types";
|
||||
import {
|
||||
combineClasses,
|
||||
deploymentStateClass,
|
||||
readableVersion,
|
||||
} from "../../util/helpers";
|
||||
import Circle from "../shared/Circle";
|
||||
import Flex from "../shared/layout/Flex";
|
||||
import Grid from "../shared/layout/Grid";
|
||||
import s from "./serverchildren.module.scss";
|
||||
|
||||
const Deployment: Component<{ id: string }> = (p) => {
|
||||
const { deployments, builds } = useAppState();
|
||||
const deployment = () => deployments.get(p.id)!;
|
||||
const [deployed_version] = createResource(() =>
|
||||
client.get_deployment_deployed_version(p.id)
|
||||
);
|
||||
const derived_image = () => {
|
||||
if (deployment().deployment.build_id) {
|
||||
const build = builds.get(deployment().deployment.build_id!);
|
||||
if (build === undefined) return "unknown";
|
||||
const version =
|
||||
deployment().state === DockerContainerState.NotDeployed
|
||||
? deployment().deployment.build_version
|
||||
? readableVersion(
|
||||
deployment().deployment.build_version!
|
||||
).replaceAll("v", "")
|
||||
: "latest"
|
||||
: deployed_version() || "unknown";
|
||||
return `${build.name}:${version}`;
|
||||
} else {
|
||||
return deployment().deployment.docker_run_args.image || "unknown";
|
||||
}
|
||||
};
|
||||
const image = () => {
|
||||
if (deployment().state === DockerContainerState.NotDeployed) {
|
||||
if (deployment().deployment.build_id) {
|
||||
const build = builds.get(deployment().deployment.build_id!);
|
||||
if (build === undefined) return "unknown"
|
||||
const version = deployment().deployment.build_version
|
||||
? readableVersion(deployment().deployment.build_version!).replaceAll(
|
||||
"v",
|
||||
""
|
||||
)
|
||||
: "latest";
|
||||
return `${build.name}:${version}`;
|
||||
} else {
|
||||
return deployment().deployment.docker_run_args.image || "unknown";
|
||||
}
|
||||
return derived_image();
|
||||
} else if (deployment().container?.image) {
|
||||
if (deployment().container!.image.includes("sha256:")) {
|
||||
return derived_image();
|
||||
}
|
||||
let [account, image] = deployment().container!.image.split("/");
|
||||
return image ? image : account;
|
||||
} else {
|
||||
@@ -39,7 +49,15 @@ const Deployment: Component<{ id: string }> = (p) => {
|
||||
};
|
||||
return (
|
||||
<Show when={deployment()}>
|
||||
<A href={`/deployment/${p.id}`} class="card hoverable" style={{ width: "100%", "justify-content": "space-between", padding: "0.5rem" }}>
|
||||
<A
|
||||
href={`/deployment/${p.id}`}
|
||||
class="card hoverable"
|
||||
style={{
|
||||
width: "100%",
|
||||
"justify-content": "space-between",
|
||||
padding: "0.5rem",
|
||||
}}
|
||||
>
|
||||
<Grid gap="0">
|
||||
<h2>{deployment().deployment.name}</h2>
|
||||
<div style={{ opacity: 0.7 }}>{image()}</div>
|
||||
|
||||
33
frontend/src/components/shared/CheckBox.tsx
Normal file
33
frontend/src/components/shared/CheckBox.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Component, JSX } from "solid-js";
|
||||
import Flex from "./layout/Flex";
|
||||
|
||||
const CheckBox: Component<{
|
||||
label: JSX.Element;
|
||||
checked: boolean;
|
||||
toggle: () => void;
|
||||
}> = (p) => {
|
||||
return (
|
||||
<button
|
||||
class="blue"
|
||||
style={{ gap: "0.5rem" }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
p.toggle();
|
||||
}}
|
||||
>
|
||||
{p.label}
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={p.checked}
|
||||
style={{
|
||||
width: "fit-content",
|
||||
margin: 0,
|
||||
appearance: "auto",
|
||||
"-webkit-appearance": "checkbox",
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckBox;
|
||||
@@ -5,6 +5,7 @@ const TextArea: Component<
|
||||
onEdit?: (value: string) => void;
|
||||
onConfirm?: (value: string) => void;
|
||||
onEnter?: (value: string) => void;
|
||||
shiftDisablesOnEnter?: boolean;
|
||||
disabled?: boolean;
|
||||
} & JSX.InputHTMLAttributes<HTMLTextAreaElement> &
|
||||
JSX.HTMLAttributes<HTMLDivElement>
|
||||
@@ -16,7 +17,11 @@ const TextArea: Component<
|
||||
onInput={(e) => p.onEdit && p.onEdit(e.currentTarget.value)}
|
||||
onBlur={(e) => p.onConfirm && p.onConfirm(e.currentTarget.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && p.onEnter) {
|
||||
if (
|
||||
e.key === "Enter" &&
|
||||
p.onEnter &&
|
||||
(!p.shiftDisablesOnEnter || !e.shiftKey)
|
||||
) {
|
||||
e.preventDefault();
|
||||
p.onEnter(e.currentTarget.value);
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ import Circle from "../../shared/Circle";
|
||||
import { ControlledTabs } from "../../shared/tabs/Tabs";
|
||||
import { useAppDimensions } from "../../../state/DimensionProvider";
|
||||
import Grid from "../../shared/layout/Grid";
|
||||
import { A, useNavigate } from "@solidjs/router";
|
||||
import { Build, ServerStatus } from "../../../types";
|
||||
import { A } from "@solidjs/router";
|
||||
import { ServerStatus } from "../../../types";
|
||||
|
||||
const mobileStyle: JSX.CSSProperties = {
|
||||
// position: "fixed",
|
||||
|
||||
330
frontend/src/components/users/User.tsx
Normal file
330
frontend/src/components/users/User.tsx
Normal file
@@ -0,0 +1,330 @@
|
||||
import { A, useParams } from "@solidjs/router";
|
||||
import {
|
||||
Component,
|
||||
createMemo,
|
||||
createResource,
|
||||
createSignal,
|
||||
For,
|
||||
onCleanup,
|
||||
Show,
|
||||
} from "solid-js";
|
||||
import { client } from "../..";
|
||||
import { useAppDimensions } from "../../state/DimensionProvider";
|
||||
import { useAppState } from "../../state/StateProvider";
|
||||
import {
|
||||
Operation,
|
||||
PermissionLevel,
|
||||
PermissionsTarget,
|
||||
User as UserType,
|
||||
} from "../../types";
|
||||
import {
|
||||
getId,
|
||||
readableMonitorTimestamp,
|
||||
readableUserType,
|
||||
} from "../../util/helpers";
|
||||
import { useToggle } from "../../util/hooks";
|
||||
import CheckBox from "../shared/CheckBox";
|
||||
import Icon from "../shared/Icon";
|
||||
import Input from "../shared/Input";
|
||||
import Flex from "../shared/layout/Flex";
|
||||
import Grid from "../shared/layout/Grid";
|
||||
import Loading from "../shared/loading/Loading";
|
||||
import Selector from "../shared/menu/Selector";
|
||||
|
||||
const User: Component = () => {
|
||||
const { isMobile } = useAppDimensions();
|
||||
const { builds, deployments, servers, ws } = useAppState();
|
||||
const params = useParams<{ id: string }>();
|
||||
const [user, { refetch }] = createResource(() =>
|
||||
client.get_user_by_id(params.id)
|
||||
);
|
||||
onCleanup(
|
||||
ws.subscribe(
|
||||
[
|
||||
Operation.ModifyUserEnabled,
|
||||
Operation.ModifyUserCreateServerPermissions,
|
||||
Operation.ModifyUserCreateBuildPermissions,
|
||||
Operation.ModifyUserPermissions,
|
||||
],
|
||||
refetch
|
||||
)
|
||||
);
|
||||
const [showAll, toggleShowAll] = useToggle(false);
|
||||
const [search, setSearch] = createSignal("");
|
||||
const _servers = createMemo(() => {
|
||||
if (showAll()) {
|
||||
return servers.filterArray((s) => s.server.name.includes(search()));
|
||||
} else {
|
||||
return servers.filterArray((s) => {
|
||||
if (!s.server.name.includes(search())) return false;
|
||||
const p = s.server.permissions?.[params.id];
|
||||
return p ? p !== PermissionLevel.None : false;
|
||||
});
|
||||
}
|
||||
});
|
||||
const _deployments = createMemo(() => {
|
||||
if (showAll()) {
|
||||
return deployments.filterArray((d) =>
|
||||
d.deployment.name.includes(search())
|
||||
);
|
||||
} else {
|
||||
return deployments.filterArray((d) => {
|
||||
if (!d.deployment.name.includes(search())) return false;
|
||||
const p = d.deployment.permissions?.[params.id];
|
||||
return p ? p !== PermissionLevel.None : false;
|
||||
});
|
||||
}
|
||||
});
|
||||
const _builds = createMemo(() => {
|
||||
if (showAll()) {
|
||||
return builds.filterArray((b) => b.name.includes(search()));
|
||||
} else {
|
||||
return builds.filterArray((b) => {
|
||||
if (!b.name.includes(search())) return false;
|
||||
const p = b.permissions?.[params.id];
|
||||
return p ? p !== PermissionLevel.None : false;
|
||||
});
|
||||
}
|
||||
});
|
||||
return (
|
||||
<Grid
|
||||
class="card shadow"
|
||||
style={{ width: "100%", "box-sizing": "border-box" }}
|
||||
>
|
||||
<Show when={user()} fallback={<Loading type="three-dot" />}>
|
||||
<Flex alignItems="center" justifyContent="space-between">
|
||||
<Flex alignItems="center">
|
||||
<A href="/users" class="grey">
|
||||
<Icon type="arrow-left" />
|
||||
</A>
|
||||
<h1>{user()?.username}</h1>
|
||||
<Show when={user()?.admin}>
|
||||
<h2 style={{ opacity: 0.7 }}>admin</h2>
|
||||
</Show>
|
||||
</Flex>
|
||||
<Flex alignItems="center">
|
||||
<CheckBox
|
||||
label="show all resources"
|
||||
checked={showAll()}
|
||||
toggle={toggleShowAll}
|
||||
/>
|
||||
<UserPermissionButtons user={user()!} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex justifyContent="space-between" alignItems="center">
|
||||
<Input
|
||||
placeholder="search resources"
|
||||
class="lightgrey"
|
||||
style={{ padding: "0.5rem" }}
|
||||
value={search()}
|
||||
onEdit={setSearch}
|
||||
/>
|
||||
<Flex>
|
||||
<Flex gap="0.5rem">
|
||||
<div class="dimmed">type:</div>
|
||||
<div>{user() ? readableUserType(user()!) : "unknown"}</div>
|
||||
</Flex>
|
||||
<Flex gap="0.5rem">
|
||||
<div class="dimmed">created:</div>
|
||||
<div>
|
||||
{user()?.created_at
|
||||
? readableMonitorTimestamp(user()?.created_at!)
|
||||
: "unknown"}
|
||||
</div>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Grid class="card light shadow">
|
||||
<Flex alignItems="center">
|
||||
<h1>servers</h1>
|
||||
<Show when={_servers()?.length === 0}>
|
||||
<div>empty</div>
|
||||
</Show>
|
||||
</Flex>
|
||||
<Grid gridTemplateColumns={isMobile() ? undefined : "1fr 1fr"}>
|
||||
<For each={_servers()}>
|
||||
{(item) => (
|
||||
<Flex
|
||||
class="card shadow"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Grid gap="0.25rem">
|
||||
<h2>{item.server.name}</h2>
|
||||
<div class="dimmed">
|
||||
{item.server.region || "unknown region"}
|
||||
</div>
|
||||
</Grid>
|
||||
<Selector
|
||||
targetClass={
|
||||
(item.server.permissions?.[params.id] || "none") !==
|
||||
"none"
|
||||
? "blue"
|
||||
: "red"
|
||||
}
|
||||
selected={item.server.permissions?.[params.id] || "none"}
|
||||
items={["none", "read", "execute", "update"]}
|
||||
onSelect={(permission) => {
|
||||
client.update_user_permissions_on_target({
|
||||
user_id: params.id,
|
||||
target_type: PermissionsTarget.Server,
|
||||
target_id: getId(item.server),
|
||||
permission: permission as PermissionLevel,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
</For>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid class="card light shadow">
|
||||
<Flex alignItems="center">
|
||||
<h1>deployments</h1>
|
||||
<Show when={_deployments()?.length === 0}>
|
||||
<div>empty</div>
|
||||
</Show>
|
||||
</Flex>
|
||||
<Grid gridTemplateColumns={isMobile() ? undefined : "1fr 1fr"}>
|
||||
<For each={_deployments()}>
|
||||
{(item) => (
|
||||
<Flex
|
||||
class="card shadow"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Grid gap="0.25rem">
|
||||
<h2>{item.deployment.name}</h2>
|
||||
<div class="dimmed">
|
||||
{servers.get(item.deployment.server_id)?.server.name ||
|
||||
"unknown"}
|
||||
</div>
|
||||
</Grid>
|
||||
<Selector
|
||||
targetClass={
|
||||
(item.deployment.permissions?.[params.id] || "none") !==
|
||||
"none"
|
||||
? "blue"
|
||||
: "red"
|
||||
}
|
||||
selected={
|
||||
item.deployment.permissions?.[params.id] || "none"
|
||||
}
|
||||
items={["none", "read", "execute", "update"]}
|
||||
onSelect={(permission) => {
|
||||
client.update_user_permissions_on_target({
|
||||
user_id: params.id,
|
||||
target_type: PermissionsTarget.Deployment,
|
||||
target_id: getId(item.deployment),
|
||||
permission: permission as PermissionLevel,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
</For>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid class="card light shadow">
|
||||
<Flex alignItems="center">
|
||||
<h1>builds</h1>
|
||||
<Show when={_builds()?.length === 0}>
|
||||
<div>empty</div>
|
||||
</Show>
|
||||
</Flex>
|
||||
<Grid gridTemplateColumns={isMobile() ? undefined : "1fr 1fr"}>
|
||||
<For each={_builds()}>
|
||||
{(item) => (
|
||||
<Flex
|
||||
class="card shadow"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<h2>{item.name}</h2>
|
||||
<Selector
|
||||
targetClass={
|
||||
(item.permissions?.[params.id] || "none") !== "none"
|
||||
? "blue"
|
||||
: "red"
|
||||
}
|
||||
selected={item.permissions?.[params.id] || "none"}
|
||||
items={["none", "read", "execute", "update"]}
|
||||
onSelect={(permission) => {
|
||||
client.update_user_permissions_on_target({
|
||||
user_id: params.id,
|
||||
target_type: PermissionsTarget.Build,
|
||||
target_id: getId(item),
|
||||
permission: permission as PermissionLevel,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
</For>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Show>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default User;
|
||||
|
||||
export const UserPermissionButtons: Component<{ user: UserType }> = (p) => {
|
||||
const { isMobile } = useAppDimensions();
|
||||
return (
|
||||
<Show when={!p.user.admin}>
|
||||
<Grid
|
||||
placeItems="center end"
|
||||
gridTemplateColumns={!isMobile() ? "auto 1fr 1fr" : undefined}
|
||||
>
|
||||
<button
|
||||
class={p.user.enabled ? "green" : "red"}
|
||||
style={{ width: isMobile() ? "11rem" : "6rem" }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
client.modify_user_enabled({
|
||||
user_id: getId(p.user),
|
||||
enabled: !p.user.enabled,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{p.user.enabled ? "enabled" : "disabled"}
|
||||
</button>
|
||||
<button
|
||||
class={p.user.create_server_permissions ? "green" : "red"}
|
||||
style={{ width: "11rem" }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
client.modify_user_create_server_permissions({
|
||||
user_id: getId(p.user),
|
||||
create_server_permissions: !p.user.create_server_permissions,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{p.user.create_server_permissions
|
||||
? "can create servers"
|
||||
: "cannot create servers"}
|
||||
</button>
|
||||
<button
|
||||
class={p.user.create_build_permissions ? "green" : "red"}
|
||||
style={{ width: "11rem" }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
client.modify_user_create_build_permissions({
|
||||
user_id: getId(p.user),
|
||||
create_build_permissions: !p.user.create_build_permissions,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{p.user.create_build_permissions
|
||||
? "can create builds"
|
||||
: "cannot create builds"}
|
||||
</button>
|
||||
</Grid>
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { A } from "@solidjs/router";
|
||||
import {
|
||||
Component,
|
||||
createMemo,
|
||||
@@ -8,18 +9,16 @@ import {
|
||||
Show,
|
||||
} from "solid-js";
|
||||
import { client } from "../..";
|
||||
import { useAppDimensions } from "../../state/DimensionProvider";
|
||||
import { useAppState } from "../../state/StateProvider";
|
||||
import { Operation } from "../../types";
|
||||
import { combineClasses, getId } from "../../util/helpers";
|
||||
import { getId } from "../../util/helpers";
|
||||
import Input from "../shared/Input";
|
||||
import Flex from "../shared/layout/Flex";
|
||||
import Grid from "../shared/layout/Grid";
|
||||
import Loading from "../shared/loading/Loading";
|
||||
import s from "./users.module.scss";
|
||||
import { UserPermissionButtons } from "./User";
|
||||
|
||||
const Users: Component<{}> = (p) => {
|
||||
const { isMobile } = useAppDimensions();
|
||||
const { ws } = useAppState();
|
||||
const [users, { refetch }] = createResource(() => client.list_users());
|
||||
onCleanup(
|
||||
@@ -60,61 +59,28 @@ const Users: Component<{}> = (p) => {
|
||||
</Flex>
|
||||
<For each={filteredUsers()}>
|
||||
{(user) => (
|
||||
<Flex class={combineClasses(s.User, "shadow")}>
|
||||
<div class={s.Username}>{user.username}</div>
|
||||
<Grid
|
||||
placeItems="center end"
|
||||
gridTemplateColumns={!isMobile() ? "1fr 1fr 1fr" : undefined}
|
||||
<Show
|
||||
when={!user.admin}
|
||||
fallback={
|
||||
<Flex class="card light shadow">
|
||||
<h2>{user.username}</h2>
|
||||
<h2 style={{ opacity: 0.7 }}>admin</h2>
|
||||
</Flex>
|
||||
}
|
||||
>
|
||||
<A
|
||||
href={`/user/${getId(user)}`}
|
||||
class="card light shadow"
|
||||
style={{
|
||||
width: "100%",
|
||||
"justify-content": "space-between",
|
||||
"align-items": "center",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
class={user.enabled ? "green" : "red"}
|
||||
style={{ width: isMobile() ? "11rem" : "6rem" }}
|
||||
onClick={() => {
|
||||
client.modify_user_enabled({
|
||||
user_id: getId(user),
|
||||
enabled: !user.enabled,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{user.enabled ? "enabled" : "disabled"}
|
||||
</button>
|
||||
<button
|
||||
class={user.create_server_permissions ? "green" : "red"}
|
||||
style={{ width: "11rem" }}
|
||||
onClick={() => {
|
||||
client.modify_user_create_server_permissions({
|
||||
user_id: getId(user),
|
||||
create_server_permissions:
|
||||
!user.create_server_permissions,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{user.create_server_permissions
|
||||
? "can create servers"
|
||||
: "cannot create servers"}
|
||||
</button>
|
||||
<button
|
||||
class={user.create_build_permissions ? "green" : "red"}
|
||||
style={{ width: "11rem" }}
|
||||
onClick={() => {
|
||||
client.modify_user_create_build_permissions({
|
||||
user_id: getId(user),
|
||||
create_build_permissions: !user.create_build_permissions,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{user.create_build_permissions
|
||||
? "can create builds"
|
||||
: "cannot create builds"}
|
||||
</button>
|
||||
{/* <ConfirmButton
|
||||
class="red"
|
||||
onConfirm={() => deleteUser(user._id!)}
|
||||
>
|
||||
<Icon type="trash" />
|
||||
</ConfirmButton> */}
|
||||
</Grid>
|
||||
</Flex>
|
||||
<h2>{user.username}</h2>
|
||||
<UserPermissionButtons user={user} />
|
||||
</A>
|
||||
</Show>
|
||||
)}
|
||||
</For>
|
||||
</Grid>
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
@use "../../style/colors.scss" as c;
|
||||
|
||||
.UsersContent {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.Users {
|
||||
width: fit-content;
|
||||
min-width: 30rem;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.Username {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.User {
|
||||
background-color: c.$lightgrey;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -232,13 +232,18 @@ export function useArrayWithId<T, O>(
|
||||
idPath: string[],
|
||||
options?: O
|
||||
) {
|
||||
let is_loaded = false;
|
||||
const [collection, set] = createSignal<T[]>();
|
||||
const load = (options?: O) => {
|
||||
query(options).then(set);
|
||||
const load = (_options?: O) => {
|
||||
if (!is_loaded || _options !== options) {
|
||||
query(_options).then((r) => {
|
||||
is_loaded = true;
|
||||
options = _options;
|
||||
set(r);
|
||||
});
|
||||
}
|
||||
};
|
||||
createEffect(() => {
|
||||
load(options);
|
||||
});
|
||||
load(options);
|
||||
const addOrUpdate = (item: T) => {
|
||||
set((items: T[] | undefined) => {
|
||||
if (items) {
|
||||
|
||||
@@ -314,6 +314,10 @@ svg {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.dimmed {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
// .hoverable {
|
||||
// transition: all 250ms ease-in-out;
|
||||
// }
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface Build {
|
||||
name: string;
|
||||
description?: string;
|
||||
permissions?: PermissionsMap;
|
||||
skip_secret_interp?: boolean;
|
||||
server_id?: string;
|
||||
aws_config?: AwsBuilderBuildConfig;
|
||||
version: Version;
|
||||
@@ -91,6 +92,7 @@ export interface AmiAccounts {
|
||||
ami_id: string;
|
||||
github?: string[];
|
||||
docker?: string[];
|
||||
secrets?: string[];
|
||||
}
|
||||
|
||||
export interface Deployment {
|
||||
@@ -99,6 +101,7 @@ export interface Deployment {
|
||||
description?: string;
|
||||
server_id: string;
|
||||
permissions?: PermissionsMap;
|
||||
skip_secret_interp?: boolean;
|
||||
docker_run_args: DockerRunArgs;
|
||||
build_id?: string;
|
||||
build_version?: Version;
|
||||
@@ -382,7 +385,7 @@ export interface User {
|
||||
|
||||
export interface ApiSecret {
|
||||
name: string;
|
||||
hash: string;
|
||||
hash?: string;
|
||||
created_at: string;
|
||||
expires?: string;
|
||||
}
|
||||
|
||||
@@ -49,11 +49,18 @@ import { generateQuery, QueryObject } from "./helpers";
|
||||
|
||||
export class Client {
|
||||
loginOptions: LoginOptions | undefined;
|
||||
monitorTitle: string | undefined;
|
||||
|
||||
constructor(private baseURL: string, public token: string | null) {}
|
||||
|
||||
async initialize() {
|
||||
this.loginOptions = await this.get_login_options();
|
||||
const [loginOptions, monitorTitle] = await Promise.all([
|
||||
this.get_login_options(),
|
||||
this.get_monitor_title(),
|
||||
]);
|
||||
this.loginOptions = loginOptions;
|
||||
this.monitorTitle = monitorTitle;
|
||||
document.title = monitorTitle;
|
||||
const params = new URLSearchParams(location.search);
|
||||
const exchange_token = params.get("token");
|
||||
if (exchange_token) {
|
||||
@@ -116,10 +123,16 @@ export class Client {
|
||||
return this.get(`/api/username/${user_id}`);
|
||||
}
|
||||
|
||||
// admin only
|
||||
list_users(): Promise<User[]> {
|
||||
return this.get("/api/users");
|
||||
}
|
||||
|
||||
// admin only
|
||||
get_user_by_id(user_id: string): Promise<User> {
|
||||
return this.get(`/api/user/${user_id}`);
|
||||
}
|
||||
|
||||
exchange_for_jwt(exchange_token: string): Promise<string> {
|
||||
return this.post("/auth/exchange", { token: exchange_token });
|
||||
}
|
||||
@@ -132,6 +145,10 @@ export class Client {
|
||||
return this.post("/api/update_description", body);
|
||||
}
|
||||
|
||||
get_monitor_title(): Promise<string> {
|
||||
return this.get("/api/title");
|
||||
}
|
||||
|
||||
// deployment
|
||||
|
||||
list_deployments(
|
||||
@@ -244,6 +261,10 @@ export class Client {
|
||||
return this.get(`/api/server/${id}/docker_accounts`);
|
||||
}
|
||||
|
||||
get_server_available_secrets(id: string): Promise<string[]> {
|
||||
return this.get(`/api/server/${id}/secrets`);
|
||||
}
|
||||
|
||||
get_server_version(id: string): Promise<string> {
|
||||
return this.get(`/api/server/${id}/version`);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
EnvironmentVar,
|
||||
ServerStatus,
|
||||
Timelength,
|
||||
User,
|
||||
Version,
|
||||
} from "../types";
|
||||
|
||||
@@ -234,3 +235,13 @@ export function readableStorageAmount(gb: number) {
|
||||
export function readableVersion(version: Version) {
|
||||
return `v${version.major}.${version.minor}.${version.patch}`;
|
||||
}
|
||||
|
||||
export function readableUserType(user: User) {
|
||||
if (user.github_id) {
|
||||
return "github"
|
||||
} else if (user.google_id) {
|
||||
return "google"
|
||||
} else {
|
||||
return "local"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,6 +435,18 @@
|
||||
resolved "https://registry.yarnpkg.com/@solidjs/router/-/router-0.6.0.tgz#27583cd0aa81a99482e0e7eddae5e214bd8bf6b6"
|
||||
integrity sha512-7ug2fzXXhvvDBL4CQyMvMM9o3dgBE6PoRh38T8UTmMnYz4rcCfROqSZc9yq+YEC96qWt5OvJgZ1Uj/4EAQXlfA==
|
||||
|
||||
"@tanstack/query-core@4.26.0":
|
||||
version "4.26.0"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.26.0.tgz#fc65c8c117e72baead3a82a1f272a4ec210c7650"
|
||||
integrity sha512-9CRqXmCH82KZDKmezoGU4FOn1Oqbzlp2/zf71n+9nC58e7NSqCIjfSCMpqQWcu9YqUcUykxZEUunOyKHVc6BJA==
|
||||
|
||||
"@tanstack/solid-query@^4.26.0":
|
||||
version "4.26.0"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/solid-query/-/solid-query-4.26.0.tgz#77aa1b60e47719075802891a1f29a0a25385bd48"
|
||||
integrity sha512-2+dXfIHy8pU0GlrkxjXi8i7z9Ff1C7dbspAo3t6X6jJK57kesGhpE8AjmgIOUrTH1XsoHkTnBKxmoq/I7ewMCQ==
|
||||
dependencies:
|
||||
"@tanstack/query-core" "4.26.0"
|
||||
|
||||
ansi-styles@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
apt-get update
|
||||
apt-get install -y ca-certificates curl gnupg lsb-release make cmake g++ python3 node-gyp build-essential libssl-dev git
|
||||
git config --global pull.rebase false
|
||||
|
||||
# install docker cli
|
||||
# mkdir -p /etc/apt/keyrings
|
||||
# curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
# echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
# chmod a+r /etc/apt/keyrings/docker.gpg
|
||||
# apt-get update
|
||||
# apt-get install -y docker-ce docker-ce-cli containerd.io
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
|
||||
# install nodejs and enable yarn
|
||||
curl -fsSL https://deb.nodesource.com/setup_19.x | bash - && apt-get install -y nodejs
|
||||
corepack enable
|
||||
@@ -1,11 +0,0 @@
|
||||
apt-get update
|
||||
apt-get install -y ca-certificates curl gnupg lsb-release git
|
||||
git config --global pull.rebase false
|
||||
|
||||
# install docker cli
|
||||
# mkdir -p /etc/apt/keyrings
|
||||
# curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
# echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
# apt-get update
|
||||
# apt-get install docker-ce docker-ce-cli
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
@@ -1,11 +1,11 @@
|
||||
[package]
|
||||
name = "db_client"
|
||||
version = "0.2.7"
|
||||
version = "0.2.9"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
types = { package = "monitor_types", path = "../types" }
|
||||
mungos = "0.3.0"
|
||||
mungos = "0.3.8"
|
||||
anyhow = "1.0"
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "monitor_helpers"
|
||||
version = "0.2.7"
|
||||
version = "0.2.9"
|
||||
edition = "2021"
|
||||
authors = ["MoghTech"]
|
||||
description = "helpers used as dependency for mogh tech monitor"
|
||||
|
||||
@@ -133,10 +133,7 @@ pub fn to_monitor_name(name: &str) -> String {
|
||||
}
|
||||
|
||||
pub fn handle_anyhow_error(err: anyhow::Error) -> (StatusCode, String) {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Internal Error: {err:#?}"),
|
||||
)
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, format!("{err:#?}"))
|
||||
}
|
||||
|
||||
pub fn generate_secret(length: usize) -> String {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "monitor_client"
|
||||
version = "0.2.7"
|
||||
version = "0.2.9"
|
||||
edition = "2021"
|
||||
authors = ["MoghTech"]
|
||||
description = "a client to interact with the monitor system"
|
||||
@@ -9,7 +9,7 @@ license = "GPL-3.0-or-later"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
monitor_types = "0.2.7"
|
||||
monitor_types = "0.2.9"
|
||||
# monitor_types = { path = "../types" }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
tokio-tungstenite = { version = "0.18", features=["native-tls"] }
|
||||
|
||||
@@ -58,6 +58,17 @@ impl MonitorClient {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_server_available_secrets(
|
||||
&self,
|
||||
server_id: &str,
|
||||
) -> anyhow::Result<Vec<String>> {
|
||||
self.get(
|
||||
&format!("/api/server/{server_id}/secrets"),
|
||||
Option::<()>::None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_server(&self, name: &str, address: &str) -> anyhow::Result<Server> {
|
||||
self.post(
|
||||
"/api/server/create",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "periphery_client"
|
||||
version = "0.2.7"
|
||||
version = "0.2.9"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -51,6 +51,12 @@ impl PeripheryClient {
|
||||
.context("failed to get docker accounts from periphery")
|
||||
}
|
||||
|
||||
pub async fn get_available_secrets(&self, server: &Server) -> anyhow::Result<Vec<String>> {
|
||||
self.get_json(server, "/secrets")
|
||||
.await
|
||||
.context("failed to get secret variable names from periphery")
|
||||
}
|
||||
|
||||
pub async fn get_system_information(
|
||||
&self,
|
||||
server: &Server,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "monitor_types"
|
||||
version = "0.2.7"
|
||||
version = "0.2.9"
|
||||
edition = "2021"
|
||||
authors = ["MoghTech"]
|
||||
description = "types for the mogh tech monitor"
|
||||
|
||||
@@ -37,6 +37,11 @@ pub struct Build {
|
||||
#[builder(setter(skip))]
|
||||
pub permissions: PermissionsMap,
|
||||
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
#[diff(attr(#[serde(skip_serializing_if = "Option::is_none")]))]
|
||||
pub skip_secret_interp: bool,
|
||||
|
||||
#[builder(default)]
|
||||
#[diff(attr(#[serde(skip_serializing_if = "option_diff_no_change")]))]
|
||||
pub server_id: Option<String>, // server which this image should be built on
|
||||
|
||||
@@ -17,6 +17,9 @@ pub type SecretsMap = HashMap<String, String>; // these are used for injection i
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct CoreConfig {
|
||||
#[serde(default = "default_title")]
|
||||
pub title: String,
|
||||
|
||||
// the host to use with oauth redirect url, whatever host the user hits to access monitor. eg 'https://monitor.mogh.tech'
|
||||
pub host: String,
|
||||
|
||||
@@ -71,6 +74,10 @@ pub struct CoreConfig {
|
||||
pub aws: AwsBuilderConfig,
|
||||
}
|
||||
|
||||
fn default_title() -> String {
|
||||
String::from("monitor")
|
||||
}
|
||||
|
||||
fn default_core_port() -> u16 {
|
||||
9000
|
||||
}
|
||||
@@ -161,6 +168,8 @@ pub struct AmiAccounts {
|
||||
pub github: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub docker: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub secrets: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
|
||||
@@ -37,6 +37,11 @@ pub struct Deployment {
|
||||
#[builder(setter(skip))]
|
||||
pub permissions: PermissionsMap,
|
||||
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
#[diff(attr(#[serde(skip_serializing_if = "Option::is_none")]))]
|
||||
pub skip_secret_interp: bool,
|
||||
|
||||
#[builder(default)]
|
||||
#[diff(attr(#[serde(skip_serializing_if = "docker_run_args_diff_no_change")]))]
|
||||
pub docker_run_args: DockerRunArgs,
|
||||
|
||||
@@ -115,11 +115,11 @@ impl Default for Server {
|
||||
}
|
||||
|
||||
fn default_cpu_alert() -> f32 {
|
||||
50.0
|
||||
95.0
|
||||
}
|
||||
|
||||
fn default_mem_alert() -> f64 {
|
||||
75.0
|
||||
80.0
|
||||
}
|
||||
|
||||
fn default_disk_alert() -> f64 {
|
||||
|
||||
@@ -71,6 +71,7 @@ pub struct User {
|
||||
#[diff(attr(#[derive(Debug, Serialize)]))]
|
||||
pub struct ApiSecret {
|
||||
pub name: String,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub hash: String,
|
||||
pub created_at: String,
|
||||
pub expires: Option<String>,
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
[Unit]
|
||||
Description=agent to connect with monitor core
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=max
|
||||
WorkingDirectory=/home/max
|
||||
ExecStart=/bin/bash --login -c 'source /home/max/.bashrc; $HOME/.cargo/bin/periphery --config-path ~/.monitor/periphery.config.toml --home-dir $HOME'
|
||||
TimeoutStartSec=0
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "monitor_periphery"
|
||||
version = "0.2.7"
|
||||
version = "0.2.9"
|
||||
edition = "2021"
|
||||
authors = ["MoghTech"]
|
||||
description = "monitor periphery binary | run monitor periphery as system daemon"
|
||||
@@ -17,12 +17,10 @@ helpers = { package = "monitor_helpers", path = "../lib/helpers" }
|
||||
types = { package = "monitor_types", path = "../lib/types" }
|
||||
run_command = { version = "0.0.5", features = ["async_tokio"] }
|
||||
async_timing_util = "0.1.14"
|
||||
tokio = { version = "1.25", features = ["full"] }
|
||||
# tokio-util = "0.7"
|
||||
tokio = { version = "1.26", features = ["full"] }
|
||||
axum = { version = "0.6", features = ["ws"] }
|
||||
tower = { version = "0.4", features = ["full"] }
|
||||
futures = "0.3"
|
||||
# futures-util = "0.3.25"
|
||||
dotenv = "0.15"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
@@ -34,3 +32,4 @@ sysinfo = "0.28"
|
||||
toml = "0.7"
|
||||
daemonize = "0.5.0"
|
||||
clap = { version = "4.1", features = ["derive"] }
|
||||
svi = "0.1.3"
|
||||
|
||||
@@ -26,7 +26,14 @@ async fn build_image(
|
||||
tokio::spawn(async move {
|
||||
let logs = match get_docker_token(&build.docker_account, &config) {
|
||||
Ok(docker_token) => {
|
||||
match docker::build(&build, config.repo_dir.clone(), docker_token).await {
|
||||
match docker::build(
|
||||
&build,
|
||||
config.repo_dir.clone(),
|
||||
docker_token,
|
||||
&config.secrets,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(logs) => logs,
|
||||
Err(e) => vec![Log::error("build", format!("{e:#?}"))],
|
||||
}
|
||||
|
||||
@@ -109,7 +109,13 @@ async fn deploy(
|
||||
) -> anyhow::Result<Json<Log>> {
|
||||
let log = match get_docker_token(&deployment.docker_run_args.docker_account, &config) {
|
||||
Ok(docker_token) => tokio::spawn(async move {
|
||||
docker::deploy(&deployment, &docker_token, config.repo_dir.clone()).await
|
||||
docker::deploy(
|
||||
&deployment,
|
||||
&docker_token,
|
||||
config.repo_dir.clone(),
|
||||
&config.secrets,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.context("failed at spawn thread for deploy")?,
|
||||
|
||||
101
periphery/src/api/guard.rs
Normal file
101
periphery/src/api/guard.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::ConnectInfo,
|
||||
http::{Request, StatusCode},
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
Json, RequestExt,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use types::{monitor_timestamp, PeripheryConfig};
|
||||
|
||||
pub async fn guard_request_by_passkey(
|
||||
req: Request<Body>,
|
||||
next: Next<Body>,
|
||||
) -> Result<Response, (StatusCode, String)> {
|
||||
let config = req.extensions().get::<Arc<PeripheryConfig>>().ok_or((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"could not get periphery config".to_string(),
|
||||
))?;
|
||||
if config.passkeys.is_empty() {
|
||||
return Ok(next.run(req).await);
|
||||
}
|
||||
let req_passkey = req.headers().get("authorization");
|
||||
if req_passkey.is_none() {
|
||||
return Err((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
format!("request was not sent with passkey"),
|
||||
));
|
||||
}
|
||||
let req_passkey = req_passkey
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
format!("failed to get passkey from authorization header as str: {e:?}"),
|
||||
)
|
||||
})?
|
||||
.to_string();
|
||||
if config.passkeys.contains(&req_passkey) {
|
||||
Ok(next.run(req).await)
|
||||
} else {
|
||||
let ConnectInfo(socket_addr) =
|
||||
req.extensions().get::<ConnectInfo<SocketAddr>>().ok_or((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"could not get socket addr of request".to_string(),
|
||||
))?;
|
||||
let ip = socket_addr.ip();
|
||||
let method = req.method().to_owned();
|
||||
let uri = req.uri().to_owned();
|
||||
let body = req
|
||||
.extract::<Json<Value>, _>()
|
||||
.await
|
||||
.ok()
|
||||
.map(|Json(body)| body);
|
||||
eprintln!(
|
||||
"{} | unauthorized request from {ip} (bad passkey) | method: {method} | uri: {uri} | body: {body:?}",
|
||||
monitor_timestamp(),
|
||||
);
|
||||
Err((StatusCode::UNAUTHORIZED, format!("request passkey invalid")))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn guard_request_by_ip(
|
||||
req: Request<Body>,
|
||||
next: Next<Body>,
|
||||
) -> Result<Response, (StatusCode, String)> {
|
||||
let config = req.extensions().get::<Arc<PeripheryConfig>>().ok_or((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"could not get periphery config".to_string(),
|
||||
))?;
|
||||
if config.allowed_ips.is_empty() {
|
||||
return Ok(next.run(req).await);
|
||||
}
|
||||
let ConnectInfo(socket_addr) = req.extensions().get::<ConnectInfo<SocketAddr>>().ok_or((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"could not get socket addr of request".to_string(),
|
||||
))?;
|
||||
let ip = socket_addr.ip();
|
||||
if config.allowed_ips.contains(&ip) {
|
||||
Ok(next.run(req).await)
|
||||
} else {
|
||||
let method = req.method().to_owned();
|
||||
let uri = req.uri().to_owned();
|
||||
let body = req
|
||||
.extract::<Json<Value>, _>()
|
||||
.await
|
||||
.ok()
|
||||
.map(|Json(body)| body);
|
||||
eprintln!(
|
||||
"{} | unauthorized request from {ip} | method: {method} | uri: {uri} | body: {body:?}",
|
||||
monitor_timestamp()
|
||||
);
|
||||
Err((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
format!("requesting ip {ip} not allowed"),
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,4 @@
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::ConnectInfo,
|
||||
http::{Request, StatusCode},
|
||||
middleware::{self, Next},
|
||||
response::Response,
|
||||
routing::get,
|
||||
Json, RequestExt, Router,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use types::{monitor_timestamp, PeripheryConfig};
|
||||
use axum::{middleware, routing::get, Json, Router};
|
||||
|
||||
use crate::{helpers::docker::DockerClient, HomeDirExtension, PeripheryConfigExtension};
|
||||
|
||||
@@ -21,6 +9,7 @@ mod build;
|
||||
mod command;
|
||||
mod container;
|
||||
mod git;
|
||||
mod guard;
|
||||
mod image;
|
||||
mod network;
|
||||
mod stats;
|
||||
@@ -34,6 +23,7 @@ pub fn router(config: PeripheryConfigExtension, home_dir: HomeDirExtension) -> R
|
||||
get(|sys: StatsExtension| async move { Json(sys.read().unwrap().info.clone()) }),
|
||||
)
|
||||
.route("/accounts/:account_type", get(accounts::get_accounts))
|
||||
.route("/secrets", get(get_available_secrets))
|
||||
.nest("/command", command::router())
|
||||
.nest("/container", container::router())
|
||||
.nest("/network", network::router())
|
||||
@@ -42,8 +32,8 @@ pub fn router(config: PeripheryConfigExtension, home_dir: HomeDirExtension) -> R
|
||||
.nest("/build", build::router())
|
||||
.nest("/image", image::router())
|
||||
.layer(DockerClient::extension())
|
||||
.layer(middleware::from_fn(guard_request_by_ip))
|
||||
.layer(middleware::from_fn(guard_request_by_passkey))
|
||||
.layer(middleware::from_fn(guard::guard_request_by_ip))
|
||||
.layer(middleware::from_fn(guard::guard_request_by_passkey))
|
||||
.layer(StatsClient::extension(
|
||||
config.stats_polling_rate.to_string().parse().unwrap(),
|
||||
))
|
||||
@@ -51,91 +41,7 @@ pub fn router(config: PeripheryConfigExtension, home_dir: HomeDirExtension) -> R
|
||||
.layer(home_dir)
|
||||
}
|
||||
|
||||
async fn guard_request_by_passkey(
|
||||
req: Request<Body>,
|
||||
next: Next<Body>,
|
||||
) -> Result<Response, (StatusCode, String)> {
|
||||
let config = req.extensions().get::<Arc<PeripheryConfig>>().ok_or((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"could not get periphery config".to_string(),
|
||||
))?;
|
||||
if config.passkeys.is_empty() {
|
||||
return Ok(next.run(req).await);
|
||||
}
|
||||
let req_passkey = req.headers().get("authorization");
|
||||
if req_passkey.is_none() {
|
||||
return Err((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
format!("request was not sent with passkey"),
|
||||
));
|
||||
}
|
||||
let req_passkey = req_passkey
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
format!("failed to get passkey from authorization header as str: {e:?}"),
|
||||
)
|
||||
})?
|
||||
.to_string();
|
||||
if config.passkeys.contains(&req_passkey) {
|
||||
Ok(next.run(req).await)
|
||||
} else {
|
||||
let ConnectInfo(socket_addr) =
|
||||
req.extensions().get::<ConnectInfo<SocketAddr>>().ok_or((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"could not get socket addr of request".to_string(),
|
||||
))?;
|
||||
let ip = socket_addr.ip();
|
||||
let method = req.method().to_owned();
|
||||
let uri = req.uri().to_owned();
|
||||
let body = req
|
||||
.extract::<Json<Value>, _>()
|
||||
.await
|
||||
.ok()
|
||||
.map(|Json(body)| body);
|
||||
eprintln!(
|
||||
"{} | unauthorized request from {ip} (bad passkey) | method: {method} | uri: {uri} | body: {body:?}",
|
||||
monitor_timestamp(),
|
||||
);
|
||||
Err((StatusCode::UNAUTHORIZED, format!("request passkey invalid")))
|
||||
}
|
||||
}
|
||||
|
||||
async fn guard_request_by_ip(
|
||||
req: Request<Body>,
|
||||
next: Next<Body>,
|
||||
) -> Result<Response, (StatusCode, String)> {
|
||||
let config = req.extensions().get::<Arc<PeripheryConfig>>().ok_or((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"could not get periphery config".to_string(),
|
||||
))?;
|
||||
if config.allowed_ips.is_empty() {
|
||||
return Ok(next.run(req).await);
|
||||
}
|
||||
let ConnectInfo(socket_addr) = req.extensions().get::<ConnectInfo<SocketAddr>>().ok_or((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"could not get socket addr of request".to_string(),
|
||||
))?;
|
||||
let ip = socket_addr.ip();
|
||||
if config.allowed_ips.contains(&ip) {
|
||||
Ok(next.run(req).await)
|
||||
} else {
|
||||
let method = req.method().to_owned();
|
||||
let uri = req.uri().to_owned();
|
||||
let body = req
|
||||
.extract::<Json<Value>, _>()
|
||||
.await
|
||||
.ok()
|
||||
.map(|Json(body)| body);
|
||||
eprintln!(
|
||||
"{} | unauthorized request from {ip} | method: {method} | uri: {uri} | body: {body:?}",
|
||||
monitor_timestamp()
|
||||
);
|
||||
Err((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
format!("requesting ip {ip} not allowed"),
|
||||
))
|
||||
}
|
||||
async fn get_available_secrets(config: PeripheryConfigExtension) -> Json<Vec<String>> {
|
||||
let vars: Vec<String> = config.secrets.keys().map(|k| k.clone()).collect();
|
||||
Json(vars)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::path::PathBuf;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use helpers::to_monitor_name;
|
||||
@@ -20,10 +20,12 @@ pub async fn build(
|
||||
docker_build_args,
|
||||
docker_account,
|
||||
docker_organization,
|
||||
skip_secret_interp,
|
||||
..
|
||||
}: &Build,
|
||||
mut repo_dir: PathBuf,
|
||||
docker_token: Option<String>,
|
||||
secrets: &HashMap<String, String>,
|
||||
) -> anyhow::Result<Vec<Log>> {
|
||||
let mut logs = Vec::new();
|
||||
let DockerBuildArgs {
|
||||
@@ -55,8 +57,19 @@ pub async fn build(
|
||||
"cd {} && docker build {build_args}{image_tags} -f {dockerfile_path} .{docker_push}",
|
||||
build_dir.display()
|
||||
);
|
||||
let build_log = run_monitor_command("docker build", command).await;
|
||||
logs.push(build_log);
|
||||
if *skip_secret_interp {
|
||||
let build_log = run_monitor_command("docker build", command).await;
|
||||
logs.push(build_log);
|
||||
} else {
|
||||
let (command, replacers) =
|
||||
svi::interpolate_variables(&command, secrets, svi::Interpolator::DoubleBrackets)
|
||||
.context("failed to interpolate secrets into docker build command")?;
|
||||
let mut build_log = run_monitor_command("docker build", command).await;
|
||||
build_log.command = svi::replace_in_string(&build_log.command, &replacers);
|
||||
build_log.stdout = svi::replace_in_string(&build_log.stdout, &replacers);
|
||||
build_log.stderr = svi::replace_in_string(&build_log.stderr, &replacers);
|
||||
logs.push(build_log);
|
||||
}
|
||||
Ok(logs)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::path::PathBuf;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use helpers::to_monitor_name;
|
||||
@@ -78,6 +78,7 @@ pub async fn deploy(
|
||||
deployment: &Deployment,
|
||||
docker_token: &Option<String>,
|
||||
repo_dir: PathBuf,
|
||||
secrets: &HashMap<String, String>,
|
||||
) -> Log {
|
||||
if let Err(e) = docker_login(&deployment.docker_run_args.docker_account, docker_token).await {
|
||||
return Log::error("docker login", format!("{e:#?}"));
|
||||
@@ -85,7 +86,22 @@ pub async fn deploy(
|
||||
let _ = pull_image(&deployment.docker_run_args.image).await;
|
||||
let _ = stop_and_remove_container(&to_monitor_name(&deployment.name)).await;
|
||||
let command = docker_run_command(deployment, repo_dir);
|
||||
run_monitor_command("docker run", command).await
|
||||
if deployment.skip_secret_interp {
|
||||
run_monitor_command("docker run", command).await
|
||||
} else {
|
||||
let command =
|
||||
svi::interpolate_variables(&command, secrets, svi::Interpolator::DoubleBrackets)
|
||||
.context("failed to interpolate secrets into docker run command");
|
||||
if let Err(e) = command {
|
||||
return Log::error("docker run", format!("{e:?}"));
|
||||
}
|
||||
let (command, replacers) = command.unwrap();
|
||||
let mut log = run_monitor_command("docker run", command).await;
|
||||
log.command = svi::replace_in_string(&log.command, &replacers);
|
||||
log.stdout = svi::replace_in_string(&log.stdout, &replacers);
|
||||
log.stderr = svi::replace_in_string(&log.stderr, &replacers);
|
||||
log
|
||||
}
|
||||
}
|
||||
|
||||
pub fn docker_run_command(
|
||||
|
||||
@@ -14,7 +14,7 @@ mod helpers;
|
||||
type PeripheryConfigExtension = Extension<Arc<PeripheryConfig>>;
|
||||
type HomeDirExtension = Extension<Arc<String>>;
|
||||
|
||||
fn main() {
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let (args, port, config, home_dir) = config::load();
|
||||
|
||||
if args.daemon {
|
||||
@@ -29,7 +29,9 @@ fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
run_periphery_server(port, config, home_dir)
|
||||
run_periphery_server(port, config, home_dir)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
@@ -37,11 +39,12 @@ async fn run_periphery_server(
|
||||
port: u16,
|
||||
config: PeripheryConfigExtension,
|
||||
home_dir: HomeDirExtension,
|
||||
) {
|
||||
) -> anyhow::Result<()> {
|
||||
let app = api::router(config, home_dir);
|
||||
|
||||
axum::Server::bind(&get_socket_addr(port))
|
||||
.serve(app.into_make_service_with_connect_info::<SocketAddr>())
|
||||
.await
|
||||
.expect("monitor periphery axum server crashed");
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user