From bbee8c646011a44b0a2e7a921241794f026b0872 Mon Sep 17 00:00:00 2001 From: Joe Bellus Date: Sat, 12 Feb 2022 01:23:07 -0500 Subject: [PATCH] Health checks Health checks can be enabled for applications which will make an http request once per minute and color the application icon based on the response and status code. --- Cargo.lock | 334 +++++++++++++++++- Cargo.toml | 5 +- Dockerfile | 2 +- .../20220203034730_create-apps-table.sql | 4 +- src/api/applications.rs | 42 +++ src/api/mod.rs | 8 +- src/entity/application.rs | 2 + src/error.rs | 2 +- src/events.rs | 81 +++++ src/main.js | 5 + src/main.rs | 58 ++- src/socket_session.rs | 79 +++++ src/ui/App.vue | 1 - .../components/ApplicationCategoryModal.vue | 2 +- src/ui/components/ApplicationModal.vue | 6 +- src/ui/components/ApplicationTile.vue | 9 +- src/ui/components/BookmarkCategoryModal.vue | 2 +- src/ui/components/BookmarkModal.vue | 2 +- src/ui/components/Button.vue | 34 +- src/ui/components/FormField.vue | 9 +- src/ui/components/IconPicker.vue | 27 +- src/ui/components/Modal.vue | 4 +- src/ui/components/Panel.vue | 2 +- src/ui/components/SelectField.vue | 7 +- src/ui/components/Switch.vue | 49 +++ src/ui/components/TextField.vue | 12 +- src/ui/views/Dashboard.vue | 38 +- src/ui/views/Login.vue | 2 +- src/ui/views/Setup.vue | 2 +- 29 files changed, 772 insertions(+), 58 deletions(-) create mode 100644 src/events.rs create mode 100644 src/socket_session.rs create mode 100644 src/ui/components/Switch.vue diff --git a/Cargo.lock b/Cargo.lock index aab47cd..564ef97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,16 +86,18 @@ dependencies = [ [[package]] name = "actix-cors" -version = "0.5.4" +version = "0.6.0-beta.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36b133d8026a9f209a9aeeeacd028e7451bcca975f592881b305d37983f303d7" +checksum = "8debd30414af03e9411186aac95e0230b0bb1e91146f48015dfab5c049940223" dependencies = [ - "actix-web 3.3.3", + "actix-service 2.0.2", + "actix-utils 3.0.0", + "actix-web 4.0.0-rc.2", "derive_more", "futures-util", "log", "once_cell", - "tinyvec", + "smallvec", ] [[package]] @@ -461,6 +463,23 @@ dependencies = [ "url", ] +[[package]] +name = "actix-web-actors" +version = "4.0.0-beta.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf2ef3eae6001ac2fa6690b2f8b152c00b5b8b2248e3e30f82dd2ec1e941345" +dependencies = [ + "actix", + "actix-codec 0.4.2", + "actix-http 3.0.0-rc.1", + "actix-web 4.0.0-rc.2", + "bytes 1.1.0", + "bytestring", + "futures-core", + "pin-project-lite 0.2.8", + "tokio 1.16.1", +] + [[package]] name = "actix-web-codegen" version = "0.4.0" @@ -852,6 +871,22 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2df960f5d869b2dd8532793fde43eb5427cceb126c929747a26823ab0eeb536" +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + [[package]] name = "cpufeatures" version = "0.2.1" @@ -996,6 +1031,15 @@ dependencies = [ "syn", ] +[[package]] +name = "fastrand" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +dependencies = [ + "instant", +] + [[package]] name = "firestorm" version = "0.5.0" @@ -1032,6 +1076,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.0.1" @@ -1308,6 +1367,17 @@ dependencies = [ "itoa 1.0.1", ] +[[package]] +name = "http-body" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" +dependencies = [ + "bytes 1.1.0", + "http", + "pin-project-lite 0.2.8", +] + [[package]] name = "httparse" version = "1.5.1" @@ -1320,6 +1390,43 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +[[package]] +name = "hyper" +version = "0.14.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ec3e62bdc98a2f0393a5048e4c30ef659440ea6e0e572965103e72bd836f55" +dependencies = [ + "bytes 1.1.0", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.11", + "http", + "http-body", + "httparse", + "httpdate", + "itoa 0.4.8", + "pin-project-lite 0.2.8", + "socket2 0.4.4", + "tokio 1.16.1", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes 1.1.0", + "hyper", + "native-tls", + "tokio 1.16.1", + "tokio-native-tls", +] + [[package]] name = "idna" version = "0.2.3" @@ -1368,9 +1475,15 @@ dependencies = [ "socket2 0.3.19", "widestring", "winapi 0.3.9", - "winreg", + "winreg 0.6.2", ] +[[package]] +name = "ipnet" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" + [[package]] name = "itertools" version = "0.10.3" @@ -1677,6 +1790,24 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "native-tls" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "net2" version = "0.2.37" @@ -1780,6 +1911,39 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "openssl" +version = "0.10.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95" +dependencies = [ + "bitflags", + "cfg-if 1.0.0", + "foreign-types", + "libc", + "once_cell", + "openssl-sys", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "ouroboros" version = "0.14.0" @@ -2099,6 +2263,51 @@ version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "reqwest" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f242f1488a539a79bac6dbe7c8609ae43b7914b7736210f239a37cccb32525" +dependencies = [ + "base64", + "bytes 1.1.0", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.3.11", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite 0.2.8", + "serde", + "serde_json", + "serde_urlencoded", + "tokio 1.16.1", + "tokio-native-tls", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg 0.7.0", +] + [[package]] name = "resolv-conf" version = "0.7.0" @@ -2222,6 +2431,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi 0.3.9", +] + [[package]] name = "scopeguard" version = "1.1.0" @@ -2324,6 +2543,29 @@ dependencies = [ "syn", ] +[[package]] +name = "security-framework" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "0.9.0" @@ -2709,6 +2951,20 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if 1.0.0", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi 0.3.9", +] + [[package]] name = "thiserror" version = "1.0.30" @@ -2864,9 +3120,31 @@ dependencies = [ "parking_lot", "pin-project-lite 0.2.8", "signal-hook-registry", + "tokio-macros", "winapi 0.3.9", ] +[[package]] +name = "tokio-macros" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio 1.16.1", +] + [[package]] name = "tokio-rustls" version = "0.22.0" @@ -2917,6 +3195,12 @@ dependencies = [ "tokio 1.16.1", ] +[[package]] +name = "tower-service" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" + [[package]] name = "tracing" version = "0.1.30" @@ -3074,6 +3358,12 @@ dependencies = [ "trust-dns-proto", ] +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + [[package]] name = "typenum" version = "1.15.0" @@ -3158,6 +3448,7 @@ dependencies = [ "actix-cors", "actix-rt 2.6.0", "actix-web 4.0.0-rc.2", + "actix-web-actors", "actix-web-httpauth", "base64", "bcrypt", @@ -3166,11 +3457,13 @@ dependencies = [ "jsonwebtoken", "mime_guess", "rand 0.8.4", + "reqwest", "rust-embed", "sea-orm", "serde", "serde_json", "sqlx", + "tokio 1.16.1", "tracing", "tracing-actix-web", "tracing-subscriber", @@ -3207,6 +3500,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" @@ -3244,6 +3547,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb6ec270a31b1d3c7e266b999739109abce8b6c87e4b31fcfcd788b65267395" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.79" @@ -3360,6 +3675,15 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "winreg" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "ws2_32-sys" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 0eaf39b..97f57ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,8 @@ tracing = "0.1.30" tracing-unwrap = "0.9.2" tracing-subscriber = { version = "0.3.8", features = ["fmt"] } actix = "0.12.0" -actix-cors = "0.5.4" +actix-cors = "0.6.0-beta.10" +actix-web-actors = "4.0.0-beta.6" chrono = { version = "0.4.19", features = ["serde"] } serde = { version = "1.0.136", features= [ "derive" ] } serde_json = "1.0.78" @@ -25,8 +26,10 @@ bcrypt = "0.10.1" actix-web-httpauth = "0.6.0-beta.7" jsonwebtoken = "8.0.1" rand = "0.8.4" +tokio = { verison = "1", features=["full"] } base64 = "0.13.0" sqlx = { version = "^0.5", features=["sqlite", "migrate"] } +reqwest = "0.11.9" [target.'cfg(all(target_env = "musl", target_pointer_width = "64"))'.dependencies.jemallocator] version = "0.3" diff --git a/Dockerfile b/Dockerfile index e5bd427..0c0e8eb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM alpine:latest COPY target/x86_64-unknown-linux-musl/release/vade /app/vade -EXPOSE 8080 +EXPOSE 8088 WORKDIR app RUN touch data.db CMD ["./vade"] diff --git a/migrations/20220203034730_create-apps-table.sql b/migrations/20220203034730_create-apps-table.sql index 83f42cf..9677b35 100644 --- a/migrations/20220203034730_create-apps-table.sql +++ b/migrations/20220203034730_create-apps-table.sql @@ -6,7 +6,9 @@ CREATE TABLE application ( description TEXT, active Boolean NOT NULL DEFAULT 1, glyph TEXT, - application_category_id INTEGER + application_category_id INTEGER, + alive Boolean NOT NULL DEFAULT 1, + enable_healthcheck Boolean NOT NULL DEFAULT 0 ); CREATE TABLE application_category ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, diff --git a/src/api/applications.rs b/src/api/applications.rs index 6835189..2031171 100644 --- a/src/api/applications.rs +++ b/src/api/applications.rs @@ -3,11 +3,50 @@ use tracing::instrument; use crate::api::api_prelude::*; use crate::error::{Error, Result}; +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct ApiApplication { + pub id: i32, + pub app_name: String, + pub url: String, + pub description: Option, + pub active: bool, + pub glyph: Option, + pub application_category_id: Option, + pub enable_healthcheck: bool, + pub healthcheck_status: Option, +} + +impl From for ApiApplication { + fn from(model: application::Model) -> Self { + Self { + id: model.id, + app_name: model.app_name, + url: model.url, + description: model.description, + active: model.active, + glyph: model.glyph, + application_category_id: model.application_category_id, + enable_healthcheck: model.enable_healthcheck, + healthcheck_status: None, + } + } +} + #[instrument] #[get("")] pub async fn list_applications(state: web::Data) -> Result { let apps: Vec = Application::find().all(&state.db).await.unwrap(); let count = apps.len(); + let mut apps = apps + .into_iter() + .map(|app| app.into()) + .collect::>(); + + for app in apps.iter_mut() { + app.healthcheck_status = state.healthcheck_status.lock().await.get(&app.id).cloned(); + } + Ok(HttpResponse::Ok().json(ListObjects::new(apps, count))) } @@ -25,6 +64,7 @@ pub async fn new_application( active: Set(data.0.active), glyph: Set(data.0.glyph), application_category_id: Set(data.0.application_category_id), + enable_healthcheck: Set(data.0.enable_healthcheck), }; let app = model.insert(&state.db).await?; Ok(HttpResponse::Ok().json(app)) @@ -65,6 +105,7 @@ pub async fn update_applications( url: Set(data.url), application_category_id: Set(data.application_category_id), glyph: Set(data.glyph), + enable_healthcheck: Set(data.enable_healthcheck), }; let model = ret.update(&state.db).await?; Ok(HttpResponse::Ok().json(model)) @@ -168,6 +209,7 @@ mod tests { description: Some("Some Application".into()), active: true, application_category_id: None, + enable_healthcheck: false, }; let state = setup_state().await?; diff --git a/src/api/mod.rs b/src/api/mod.rs index 16a4954..5bf6b6f 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -78,6 +78,8 @@ mod api_prelude { #[cfg(test)] pub mod test_prelude { + use std::collections::HashMap; + pub use super::ListObjects; use crate::auth; pub use crate::entity::*; @@ -96,6 +98,7 @@ pub mod test_prelude { entity::prelude::*, entity::*, tests_cfg::*, DatabaseBackend, MockDatabase, MockExecResult, Transaction, }; + use tokio::sync::Mutex; /// Sets up a testing state with an in-memory database and creates the scheme. pub async fn setup_state() -> Result> { @@ -118,7 +121,10 @@ pub mod test_prelude { auth::generate_secret(&db).await?; - Ok(actix_web::web::Data::new(AppState { db })) + Ok(actix_web::web::Data::new(AppState { + db, + healthcheck_status: Mutex::new(HashMap::new()), + })) } } diff --git a/src/entity/application.rs b/src/entity/application.rs index eecf7ab..84ac8f8 100644 --- a/src/entity/application.rs +++ b/src/entity/application.rs @@ -14,6 +14,7 @@ pub struct Model { pub active: bool, pub glyph: Option, pub application_category_id: Option, + pub enable_healthcheck: bool, } #[derive(Copy, Clone, Debug, EnumIter)] @@ -50,6 +51,7 @@ impl Default for Model { active: true, glyph: Default::default(), application_category_id: Default::default(), + enable_healthcheck: false, } } } diff --git a/src/error.rs b/src/error.rs index 008bb95..475a2e7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -62,7 +62,7 @@ impl actix_web::error::ResponseError for Error { fn status_code(&self) -> actix_web::http::StatusCode { match self.code { ErrorCode::UnAuthorized => StatusCode::UNAUTHORIZED, - ErrorCode::NoSetup => StatusCode::UPGRADE_REQUIRED, + ErrorCode::NoSetup => StatusCode::OK, _ => StatusCode::INTERNAL_SERVER_ERROR, } } diff --git a/src/events.rs b/src/events.rs new file mode 100644 index 0000000..780fee9 --- /dev/null +++ b/src/events.rs @@ -0,0 +1,81 @@ +use actix::*; +use rand::{self, rngs::ThreadRng, Rng}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +pub type ID = i64; + +#[derive(Debug, Clone)] +pub struct Session { + pub addr: Recipient, +} + +#[derive(Message)] +#[rtype(result = "ID")] +pub struct Connect { + pub addr: Recipient, +} + +#[derive(Message)] +#[rtype(result = "()")] +pub struct Disconnect { + pub id: ID, +} + +#[derive(Message, Clone, Debug, Serialize, Deserialize)] +#[rtype(result = "()")] +pub enum Event { + HealthcheckChange { app_id: i32, alive: bool }, +} + +#[derive(Default)] +pub struct EventBroker { + rng: ThreadRng, + sessions: HashMap, +} + +impl Supervised for EventBroker {} + +impl SystemService for EventBroker {} + +impl Actor for EventBroker { + type Context = Context; + + fn started(&mut self, _: &mut Self::Context) { + tracing::info!("EventBroker - Started"); + } + + fn stopped(&mut self, _: &mut Self::Context) { + tracing::error!("EventBroker - Shutdown"); + } +} + +impl Handler for EventBroker { + type Result = ID; + fn handle(&mut self, msg: Connect, _: &mut Context) -> Self::Result { + tracing::info!("Session connected"); + let id = self.rng.gen::(); + self.sessions.insert(id, Session { addr: msg.addr }); + id + } +} + +impl Handler for EventBroker { + type Result = (); + + fn handle(&mut self, msg: Disconnect, _: &mut Context) { + tracing::info!("Session disconnected"); + self.sessions.remove(&msg.id); + } +} + +impl Handler for EventBroker { + type Result = (); + + fn handle(&mut self, msg: Event, _ctx: &mut Self::Context) -> Self::Result { + tracing::info!("Event received"); + for (_, ses) in self.sessions.iter() { + let _ = ses.addr.do_send(msg.clone()); + } + } +} diff --git a/src/main.js b/src/main.js index c5914a4..b016488 100644 --- a/src/main.js +++ b/src/main.js @@ -13,6 +13,11 @@ axios.interceptors.request.use(function (config) { return config; }); + +axios.defaults.baseURL = process.env.NODE_ENV === 'development' + ? 'http://localhost:8088' + : ''; + axios.interceptors.response.use(function (response) { return response; }, function (error) { diff --git a/src/main.rs b/src/main.rs index afaf998..1475ba2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,10 @@ -use std::path::Path; +use std::{collections::HashMap, path::Path}; +use actix::SystemService; use actix_web::{get, web, App, HttpResponse, HttpServer}; use rust_embed::RustEmbed; -use sea_orm::{Database, DatabaseConnection}; +use sea_orm::{prelude::*, Database}; +use tokio::sync::Mutex; use tracing::{info, instrument}; use tracing_subscriber::prelude::*; @@ -10,6 +12,8 @@ mod api; mod auth; mod entity; mod error; +mod events; +mod socket_session; #[cfg(all(target_env = "musl", target_pointer_width = "64"))] #[global_allocator] @@ -18,32 +22,74 @@ static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc; #[derive(Debug)] pub struct AppState { pub db: DatabaseConnection, + pub healthcheck_status: Mutex>, } -#[actix_web::main] +#[actix_rt::main] async fn main() { let subscriber = tracing_subscriber::registry().with( tracing_subscriber::fmt::Layer::new() .pretty() .with_writer(std::io::stdout) .with_ansi(true) - .with_filter(tracing_subscriber::filter::LevelFilter::TRACE), + .with_filter(tracing_subscriber::filter::LevelFilter::INFO), ); tracing::subscriber::set_global_default(subscriber).expect("Unable to set a global collector"); let db = setup_database().await.unwrap(); - let state = web::Data::new(AppState { db }); + let state = web::Data::new(AppState { + db, + healthcheck_status: Mutex::new(HashMap::new()), + }); info!("Starting http server on 8080"); + let st = state.clone(); + actix_rt::spawn(async move { + loop { + tokio::time::sleep(tokio::time::Duration::from_secs(60)).await; + let apps: Vec = entity::application::Entity::find() + .filter(entity::application::Column::EnableHealthcheck.eq(true)) + .all(&st.db) + .await + .unwrap(); + + for app in apps { + match reqwest::get(app.url).await { + Ok(res) if res.status() == 200 => { + st.healthcheck_status.lock().await.insert(app.id, true); + let _ = events::EventBroker::from_registry() + .send(events::Event::HealthcheckChange { + app_id: app.id, + alive: true, + }) + .await; + } + _ => { + st.healthcheck_status.lock().await.insert(app.id, false); + let _ = events::EventBroker::from_registry() + .send(events::Event::HealthcheckChange { + app_id: app.id, + alive: false, + }) + .await; + } + } + } + } + }); + HttpServer::new(move || { + let cors = actix_cors::Cors::permissive(); App::new() .app_data(state.clone()) .wrap(tracing_actix_web::TracingLogger::default()) + .wrap(cors) + .service(web::resource("/events/{token}").to(socket_session::event_session_index)) .service(api::routes()) .service(dist) }) - .bind("0.0.0.0:8080") + .bind("0.0.0.0:8088") .unwrap() .run() .await diff --git a/src/socket_session.rs b/src/socket_session.rs new file mode 100644 index 0000000..70a4a87 --- /dev/null +++ b/src/socket_session.rs @@ -0,0 +1,79 @@ +use super::events::{Connect, Disconnect, Event, EventBroker, ID}; +use crate::auth::AuthClaims; +use actix::*; +use actix_web::{ + web::{self, Data}, + Error, HttpRequest, HttpResponse, +}; +use actix_web_actors::ws; +use jsonwebtoken::{decode, DecodingKey, Validation}; + +pub struct EventSession { + id: ID, +} + +impl Actor for EventSession { + type Context = ws::WebsocketContext; + + fn started(&mut self, ctx: &mut Self::Context) { + let addr = ctx.address(); + EventBroker::from_registry() + .send(Connect { + addr: addr.recipient(), + }) + .into_actor(self) + .then(|res, act, ctx| { + match res { + Ok(res) => act.id = res, + // something is wrong. Burn it with fire. + _ => ctx.stop(), + } + fut::ready(()) + }) + .wait(ctx); + } + fn stopping(&mut self, _: &mut Self::Context) -> Running { + // notify suprivisor we are leavin + EventBroker::from_registry().do_send(Disconnect { id: self.id }); + Running::Stop + } +} + +impl Handler for EventSession { + type Result = (); + + fn handle(&mut self, msg: Event, ctx: &mut Self::Context) -> Self::Result { + let data = serde_json::to_string(&msg).unwrap_or_default(); + ctx.text(data); + } +} + +impl StreamHandler> for EventSession { + fn handle(&mut self, msg: Result, ctx: &mut Self::Context) { + match msg { + Ok(ws::Message::Ping(msg)) => ctx.pong(&msg), + Ok(ws::Message::Text(text)) => ctx.text(text), + Ok(ws::Message::Binary(bin)) => ctx.binary(bin), + _ => (), + } + } +} + +pub async fn event_session_index( + req: HttpRequest, + stream: web::Payload, + state: Data, + path: web::Path, +) -> Result { + let secret = crate::auth::get_secret(&state.db).await?; + + let decoded = decode::( + &path, + &DecodingKey::from_secret(&secret), + &Validation::default(), + ); + match decoded { + Ok(_) => ws::start(EventSession { id: 0 }, &req, stream), + Err(e) => Err(actix_web::error::ErrorUnauthorized(e.to_string())), + } +} diff --git a/src/ui/App.vue b/src/ui/App.vue index b00271a..8bd8d5f 100644 --- a/src/ui/App.vue +++ b/src/ui/App.vue @@ -30,7 +30,6 @@ label { color: #fff; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - color: #2c3e50; } * { diff --git a/src/ui/components/ApplicationCategoryModal.vue b/src/ui/components/ApplicationCategoryModal.vue index ebaf570..c90d62e 100644 --- a/src/ui/components/ApplicationCategoryModal.vue +++ b/src/ui/components/ApplicationCategoryModal.vue @@ -9,7 +9,7 @@ diff --git a/src/ui/components/ApplicationModal.vue b/src/ui/components/ApplicationModal.vue index 398b1cf..6979231 100644 --- a/src/ui/components/ApplicationModal.vue +++ b/src/ui/components/ApplicationModal.vue @@ -13,12 +13,13 @@ + @@ -29,9 +30,10 @@ import IconPicker from "./IconPicker.vue"; import axios from "axios"; import Btn from "./Button.vue"; import SelectField from "./SelectField.vue"; +import SwitchField from './Switch.vue'; export default { - components: { TextField, Modal, IconPicker, Btn, SelectField }, + components: { TextField, Modal, IconPicker, Btn, SelectField, SwitchField }, props: ["open", "mode", "data", "categories"], watch: { data: function (next) { diff --git a/src/ui/components/ApplicationTile.vue b/src/ui/components/ApplicationTile.vue index 6892a7b..67fae12 100644 --- a/src/ui/components/ApplicationTile.vue +++ b/src/ui/components/ApplicationTile.vue @@ -1,5 +1,5 @@ diff --git a/src/ui/components/BookmarkModal.vue b/src/ui/components/BookmarkModal.vue index 60643bc..1ef63bc 100644 --- a/src/ui/components/BookmarkModal.vue +++ b/src/ui/components/BookmarkModal.vue @@ -15,7 +15,7 @@ diff --git a/src/ui/components/Button.vue b/src/ui/components/Button.vue index a2a4382..4ad68da 100644 --- a/src/ui/components/Button.vue +++ b/src/ui/components/Button.vue @@ -1,5 +1,5 @@ @@ -16,26 +17,33 @@ button { cursor: pointer; transition: all 0.2s; padding: 10px 18px; - background: #333; color: #fff; font-weight: 500; - border: 2px solid #222; - background: #333; - box-shadow: none; -} - -button.danger { - color: #cc0000; - background-color: #200; - border: 2px solid #200; + border: none; + background: #21242B; + box-shadow: rgba(0, 0, 0, 0.12) 0px 1px 3px, rgba(0, 0, 0, 0.24) 0px 1px 2px; } button:hover { transition: all 0.2s; - background: #444; + box-shadow: rgba(0, 0, 0, 0.42) 0px 1px 3px, rgba(0, 0, 0, 0.64) 0px 1px 2px; } +button.danger { + background: #41141B; +} + + button.danger:hover { - background: #400; + background: #61141B; +} + +button.primary { + background: #21244B; +} + + +button.primary:hover { + background: #21246B; } diff --git a/src/ui/components/FormField.vue b/src/ui/components/FormField.vue index 04a5112..15e89ad 100644 --- a/src/ui/components/FormField.vue +++ b/src/ui/components/FormField.vue @@ -14,10 +14,15 @@ } label { margin-right: 15px; - font-weight: bold; - text-transform: uppercase; + font-weight: 500; + line-height: 35px; + text-align: right; + color: #999; width: 150px; } +label::after { + content: ':' +} .form-field + .form-field { margin-top: 15px; } diff --git a/src/ui/components/IconPicker.vue b/src/ui/components/IconPicker.vue index a2837b6..930ba1f 100644 --- a/src/ui/components/IconPicker.vue +++ b/src/ui/components/IconPicker.vue @@ -12,6 +12,7 @@
i.includes(this.modelValue.toString().toLowerCase())) - .slice(0, 21); + .slice(0, 18); }, }, methods: { @@ -1061,19 +1062,35 @@ export default { input { color: #fff; - background-color: #121212; + background-color: transparent; outline: none; display: block; - border: 1px solid #000; + border: none; + border-bottom: 1px solid #555; + font-weight: bold; padding: 8px 15px; flex: 1; } +input:focus { + transition: all 0.2s; + border-bottom: 1px solid #336699; +} + .icons { + margin-bottom: 15px; + height: 130px; + background: #21242b; + padding: 15px; display: grid; - width: 100%; + border-radius: 10px; + width: calc(100% - 30px); margin-top: 15px; - grid-template-columns: repeat(7, 1fr); + grid-template-columns: repeat(6, 1fr); + justify-items: center; gap: 10px; } +.icons .selected { + color: #336699; +} diff --git a/src/ui/components/Modal.vue b/src/ui/components/Modal.vue index d3a798a..2a61054 100644 --- a/src/ui/components/Modal.vue +++ b/src/ui/components/Modal.vue @@ -80,7 +80,7 @@ export default { height 300ms cubic-bezier(0.4, 0, 0.2, 1); box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1), 0 6px 6px rgba(0, 0, 0, 0.16); - background: #333; + background: #282c33; display: flex; flex-direction: column; align-items: stretch; @@ -96,7 +96,7 @@ export default { } .modal .actions { - background: #3c3c3c; + background: #21242b; border-top: 1px solid #2c2c2c; display: flex; flex-direction: row; diff --git a/src/ui/components/Panel.vue b/src/ui/components/Panel.vue index cfe754f..1774b82 100644 --- a/src/ui/components/Panel.vue +++ b/src/ui/components/Panel.vue @@ -7,7 +7,7 @@ diff --git a/src/ui/components/TextField.vue b/src/ui/components/TextField.vue index 192d753..d962773 100644 --- a/src/ui/components/TextField.vue +++ b/src/ui/components/TextField.vue @@ -29,11 +29,19 @@ export default { diff --git a/src/ui/views/Dashboard.vue b/src/ui/views/Dashboard.vue index 5236f42..0d7dfd5 100644 --- a/src/ui/views/Dashboard.vue +++ b/src/ui/views/Dashboard.vue @@ -1,5 +1,5 @@