forked from github-starred/komodo
* add some network stuff to container summary * improve settings tables UI * periphery build supports additional tags * fix variable container sizing * alert types newline wrap * plumbing for Stack resource * plumbing for Stack resource * mount stack api * stack resource sync * get remote compose file * support image_name and image_tag * add server config placeholders. default server config address * configure image name and image tag * deployment work with build image_name and image_tag * stack UI * fe builds * configure registry provider and account * implement periphery stack api * stack poll interval * add UI provider management * deploy stacks * build push commit hash tag. * Destroy stack * update default core port to 9120 * remove git_account alias * finish stack (and container) api * frontend builds * cant cancel server based builds * fix * use git pull -f * 9120 * start UI updates (#15) * fix From<Stack> for CloneArgs * remove unused imports * UI Updates (#16) * cleanup dashboard charts for resources * bring back solid scrollbars * enable sidebar scrolling * remove alerts from all resources * pass jwt secret * stacks dont delete the target * parse services from yaml * stacks deploy * close * looking good * closer * destroy stack when file missing. onboard stacks * figure out stack container name matching * get stack state correct * work with service views * UI Updates - Sidebar, Topbar Alerts, and All Resources page (#17) * move sidebar to use fixed positioning instead of sticky * add alert details dialog to topbar alerts * cleanup all resources page layout * ensure resource links don't propagate clicks * periphery support passing env with --env-file * StackServicePage * default run_directory to ./ for clarify * add stack webhook listeners * add default compose name of stack name * stacks controlled with project name * migrate to dotenvy * add stack to dashboard * remove deploying / destroying stack services * update config files * fix getting service logs * git / docker provider management api * implement passing git / registry token from db * rename system user Github to Git Webhook * seperate deployed and latest services on stack info * add stack service level operations * UI Updates - Update Shadcn/UI components, prevent navbar menu layout shift (#20) * add dashboard pie for resource syncs * dashboard items same height * update shadcn components * ensure centered following sheet update * cleanup layout, prevent navbar menu layout shifts * add manual filter, fix toast call * guard webhooks * remove deployed_message, latest_message from StackListItemInfo * stop all containers on server correctly * support multiple compose files * cache all containers networks images projects * remove project missing from db cache * work on sync deploy stuff * rework deployment sync deploy to support stacks. they can depend on each other. * UI Updates - Remove topbar transparency, pretty status badges, tidy resource page layout with a 'back' button (#21) * remove topbar transparency * cleanup unused * responsive dashboard * better mobile header * dont need to calc 64px less since header is using position fixed * add status badge component * update status badges * further simplify layout * allow undefined status as prop * use new status badges for alerts * update status badges for all resources * undo layout change * tidy up resource page layout, add back button * no need for button wrapper * remove unused * build cancel log * update ts types * fix fe type changes * fe tweaks * remove on build logs * core refresh cache immediately on startup * jwt_ttl * canonicalize run directory on host * update canonicalize error message * core use docker-compose * fix incorrect project missing, add status string to stack info * remove entries in "after" that aren't deploying * fix dockerfiel * build custom tag postfix * sync fixes * ensure UpdateGitProviderAccount doesn't change id * ensure UpdateDockerRegistryAccount doesn't change id * configure providers in the UI * add // comment support to env, conversions * add updates for provider deletes * improve sync pending deploy log * add more deployment actions * add backward compat with v1.12 for clone repo * stack deploy format * fe * alert menus clone when click resource link * rename stacks * don't close on click * snake case stack state, in line with deployment state * sync redeploy stack if newer hash (optional behind resource field 'latest_hash') * remove nav to tree * RefreshStack/Sync debug instruments * improve inline UI docs * implement resource base_permission backend * plumbing for Repo build * build repos * write env file repos * add latest hash / message to build info * add optional hash to update * keep built_hash updated * add backend for build / repo latest hash management * remove unused resources * clean up repo dirs after cache update * fix repo info deser error * add build / repo git status * fix page layouts * improve layout responsive * most config incline docs * add descriptions for all resource types * default local auth false * fix omnibar arrow keys issue * add compose file to example config * image registry * dashboard display no resources messge * update deps. * show when no config * resource sync use config git_provider * fix networks * fix deploy error due to after * update lots of docs * fix server stat charts not working * update screenshots * update changelog * add a disclaimer * remove file paths docs stuff * build repo * v1.13 - Komodo * update docs for cli * fill out the compose example more --------- Co-authored-by: Karamvir Singh <67458484+karamvirsingh98@users.noreply.github.com>
201 lines
5.1 KiB
Rust
201 lines
5.1 KiB
Rust
use std::sync::OnceLock;
|
|
|
|
use anyhow::{anyhow, Context};
|
|
use jwt::Token;
|
|
use monitor_client::entities::config::core::{
|
|
CoreConfig, OauthCredentials,
|
|
};
|
|
use reqwest::StatusCode;
|
|
use serde::{de::DeserializeOwned, Deserialize};
|
|
use serde_json::Value;
|
|
use tokio::sync::Mutex;
|
|
|
|
use crate::{
|
|
auth::STATE_PREFIX_LENGTH, config::core_config,
|
|
helpers::random_string,
|
|
};
|
|
|
|
pub fn google_oauth_client() -> &'static Option<GoogleOauthClient> {
|
|
static GOOGLE_OAUTH_CLIENT: OnceLock<Option<GoogleOauthClient>> =
|
|
OnceLock::new();
|
|
GOOGLE_OAUTH_CLIENT
|
|
.get_or_init(|| GoogleOauthClient::new(core_config()))
|
|
}
|
|
|
|
pub struct GoogleOauthClient {
|
|
http: reqwest::Client,
|
|
client_id: String,
|
|
client_secret: String,
|
|
redirect_uri: String,
|
|
scopes: String,
|
|
states: Mutex<Vec<String>>,
|
|
user_agent: String,
|
|
}
|
|
|
|
impl GoogleOauthClient {
|
|
pub fn new(
|
|
CoreConfig {
|
|
google_oauth:
|
|
OauthCredentials {
|
|
enabled,
|
|
id,
|
|
secret,
|
|
},
|
|
host,
|
|
..
|
|
}: &CoreConfig,
|
|
) -> Option<GoogleOauthClient> {
|
|
if !enabled {
|
|
return None;
|
|
}
|
|
if host.is_empty() {
|
|
warn!("google oauth is enabled, but 'config.host' is not configured");
|
|
return None;
|
|
}
|
|
if id.is_empty() {
|
|
warn!("google oauth is enabled, but 'config.google_oauth.id' is not configured");
|
|
return None;
|
|
}
|
|
if secret.is_empty() {
|
|
warn!("google oauth is enabled, but 'config.google_oauth.secret' is not configured");
|
|
return None;
|
|
}
|
|
let scopes = urlencoding::encode(
|
|
&[
|
|
"https://www.googleapis.com/auth/userinfo.profile",
|
|
"https://www.googleapis.com/auth/userinfo.email",
|
|
]
|
|
.join(" "),
|
|
)
|
|
.to_string();
|
|
GoogleOauthClient {
|
|
http: Default::default(),
|
|
client_id: id.clone(),
|
|
client_secret: secret.clone(),
|
|
redirect_uri: format!("{host}/auth/google/callback"),
|
|
user_agent: String::from("monitor"),
|
|
states: Default::default(),
|
|
scopes,
|
|
}
|
|
.into()
|
|
}
|
|
|
|
#[instrument(level = "debug", skip(self))]
|
|
pub async fn get_login_redirect_url(
|
|
&self,
|
|
redirect: Option<String>,
|
|
) -> String {
|
|
let state_prefix = random_string(STATE_PREFIX_LENGTH);
|
|
let state = match redirect {
|
|
Some(redirect) => format!("{state_prefix}{redirect}"),
|
|
None => state_prefix,
|
|
};
|
|
let redirect_url = format!(
|
|
"https://accounts.google.com/o/oauth2/v2/auth?response_type=code&state={state}&client_id={}&redirect_uri={}&scope={}",
|
|
self.client_id, self.redirect_uri, self.scopes
|
|
);
|
|
let mut states = self.states.lock().await;
|
|
states.push(state);
|
|
redirect_url
|
|
}
|
|
|
|
#[instrument(level = "debug", skip(self))]
|
|
pub async fn check_state(&self, state: &str) -> bool {
|
|
let mut contained = false;
|
|
self.states.lock().await.retain(|s| {
|
|
if s.as_str() == state {
|
|
contained = true;
|
|
false
|
|
} else {
|
|
true
|
|
}
|
|
});
|
|
contained
|
|
}
|
|
|
|
#[instrument(level = "debug", skip(self))]
|
|
pub async fn get_access_token(
|
|
&self,
|
|
code: &str,
|
|
) -> anyhow::Result<AccessTokenResponse> {
|
|
self
|
|
.post::<_>(
|
|
"https://oauth2.googleapis.com/token",
|
|
&[
|
|
("client_id", self.client_id.as_str()),
|
|
("client_secret", self.client_secret.as_str()),
|
|
("redirect_uri", self.redirect_uri.as_str()),
|
|
("code", code),
|
|
("grant_type", "authorization_code"),
|
|
],
|
|
None,
|
|
)
|
|
.await
|
|
.context("failed to get google access token using code")
|
|
}
|
|
|
|
#[instrument(level = "debug", skip(self))]
|
|
pub fn get_google_user(
|
|
&self,
|
|
id_token: &str,
|
|
) -> anyhow::Result<GoogleUser> {
|
|
let t: Token<Value, GoogleUser, jwt::Unverified> =
|
|
Token::parse_unverified(id_token)
|
|
.context("failed to parse id_token")?;
|
|
Ok(t.claims().to_owned())
|
|
}
|
|
|
|
#[instrument(level = "debug", skip(self))]
|
|
async fn post<R: DeserializeOwned>(
|
|
&self,
|
|
endpoint: &str,
|
|
body: &[(&str, &str)],
|
|
bearer_token: Option<&str>,
|
|
) -> anyhow::Result<R> {
|
|
let mut req = self
|
|
.http
|
|
.post(endpoint)
|
|
.form(body)
|
|
.header("Accept", "application/json")
|
|
.header("User-Agent", &self.user_agent);
|
|
|
|
if let Some(bearer_token) = bearer_token {
|
|
req =
|
|
req.header("Authorization", format!("Bearer {bearer_token}"));
|
|
}
|
|
|
|
let res = req.send().await.context("failed to reach google")?;
|
|
|
|
let status = res.status();
|
|
|
|
if status == StatusCode::OK {
|
|
let body = res
|
|
.json()
|
|
.await
|
|
.context("failed to parse POST body into expected type")?;
|
|
Ok(body)
|
|
} else {
|
|
let text = res.text().await.context(format!(
|
|
"method: POST | status: {status} | failed to get response text"
|
|
))?;
|
|
Err(anyhow!("method: POST | status: {status} | text: {text}"))
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct AccessTokenResponse {
|
|
// pub access_token: String,
|
|
pub id_token: String,
|
|
// pub scope: String,
|
|
// pub token_type: String,
|
|
}
|
|
|
|
#[derive(Deserialize, Clone)]
|
|
pub struct GoogleUser {
|
|
#[serde(rename = "sub")]
|
|
pub id: String,
|
|
pub email: String,
|
|
pub picture: String,
|
|
}
|