From 6830c0c20d6e7de163846aedc8bc77c6191f3672 Mon Sep 17 00:00:00 2001 From: Eugene Date: Sun, 3 Jul 2022 11:56:41 +0200 Subject: [PATCH] Paginate sessions list, added filtering (#161) --- Cargo.lock | 4 +- Cargo.toml | 2 +- warpgate-admin/src/api/mod.rs | 1 + warpgate-admin/src/api/pagination.rs | 55 +++++++ warpgate-admin/src/api/sessions_list.rs | 66 +++++++-- warpgate-admin/src/api/ssh_keys.rs | 2 +- warpgate-admin/src/lib.rs | 4 + warpgate-common/src/config_providers/file.rs | 1 - warpgate-common/src/lib.rs | 2 +- warpgate-common/src/protocols/handle.rs | 8 +- warpgate-common/src/state.rs | 44 +++++- warpgate-protocol-http/src/common.rs | 7 +- warpgate-protocol-http/src/lib.rs | 9 +- warpgate-protocol-http/src/session.rs | 32 ++-- warpgate-protocol-ssh/src/helpers.rs | 2 +- warpgate-protocol-ssh/src/server/mod.rs | 15 +- warpgate-protocol-ssh/src/server/session.rs | 5 +- warpgate-web/package.json | 2 + warpgate-web/src/admin/App.svelte | 1 - warpgate-web/src/admin/Home.svelte | 137 ++++++++++-------- warpgate-web/src/admin/SSH.svelte | 6 +- .../src/admin/lib/openapi-schema.json | 64 +++++++- warpgate-web/src/common/CopyButton.svelte | 60 ++++++++ warpgate-web/src/common/ItemList.svelte | 72 +++++++++ warpgate-web/src/common/Pagination.svelte | 49 +++++++ warpgate-web/src/common/autosave.ts | 13 ++ warpgate-web/src/gateway/TargetList.svelte | 13 +- warpgate-web/src/theme/_theme.scss | 6 +- warpgate-web/src/theme/theme.dark.scss | 4 + warpgate-web/src/theme/theme.light.scss | 4 + warpgate-web/src/theme/vars.common.scss | 8 + warpgate-web/src/theme/vars.dark.scss | 7 +- warpgate-web/src/theme/vars.light.scss | 3 + warpgate-web/yarn.lock | 10 ++ 34 files changed, 590 insertions(+), 128 deletions(-) create mode 100644 warpgate-admin/src/api/pagination.rs create mode 100644 warpgate-web/src/common/CopyButton.svelte create mode 100644 warpgate-web/src/common/ItemList.svelte create mode 100644 warpgate-web/src/common/Pagination.svelte create mode 100644 warpgate-web/src/common/autosave.ts create mode 100644 warpgate-web/src/theme/vars.common.scss diff --git a/Cargo.lock b/Cargo.lock index 061384b..784724c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2708,9 +2708,9 @@ dependencies = [ [[package]] name = "russh-keys" -version = "0.22.0-beta.2" +version = "0.22.0-beta.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cead36ac2cc5b61c6774a47568117980d3df4ccf1e3dd504614809832c0781b" +checksum = "2163fb2fcdc1f03f8b37dd93392779461f4181c3a109edf0ffb1bb03e6efbff4" dependencies = [ "aes", "bcrypt-pbkdf", diff --git a/Cargo.toml b/Cargo.toml index df0c356..70eeafe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,7 +45,7 @@ poem-openapi = { version = "^1.3.30", features = [ "static-files", ] } russh = { version = "0.34.0-beta.5", features = ["openssl"] } -russh-keys = { version = "0.22.0-beta.2", features = ["openssl"] } +russh-keys = { version = "0.22.0-beta.3", features = ["openssl"] } rust-embed = "6.3" time = "0.3" tokio = { version = "1.19", features = ["tracing", "signal", "macros"] } diff --git a/warpgate-admin/src/api/mod.rs b/warpgate-admin/src/api/mod.rs index f098273..f93a140 100644 --- a/warpgate-admin/src/api/mod.rs +++ b/warpgate-admin/src/api/mod.rs @@ -3,6 +3,7 @@ use poem_openapi::OpenApi; pub mod known_hosts_detail; pub mod known_hosts_list; pub mod logs; +mod pagination; pub mod recordings_detail; pub mod sessions_detail; pub mod sessions_list; diff --git a/warpgate-admin/src/api/pagination.rs b/warpgate-admin/src/api/pagination.rs new file mode 100644 index 0000000..3bfcdac --- /dev/null +++ b/warpgate-admin/src/api/pagination.rs @@ -0,0 +1,55 @@ +use poem_openapi::types::{ParseFromJSON, ToJSON}; +use poem_openapi::Object; +use sea_orm::{ConnectionTrait, EntityTrait, FromQueryResult, PaginatorTrait, QuerySelect, Select}; + +#[derive(Object)] +#[oai(inline)] +pub struct PaginatedResponse { + items: Vec, + offset: u64, + total: u64, +} + +pub struct PaginationParams { + pub offset: Option, + pub limit: Option, +} + +impl PaginatedResponse { + pub async fn new( + query: Select, + params: PaginationParams, + db: &'_ C, + postprocess: P, + ) -> poem::Result> + where + E: EntityTrait, + C: ConnectionTrait, + M: FromQueryResult + Sized + Send + Sync + 'static, + P: FnMut(E::Model) -> T, + { + let offset = params.offset.unwrap_or(0); + let limit = params.limit.unwrap_or(100); + + let paginator = query.clone().paginate(db, limit as usize); + + let total = paginator + .num_items() + .await + .map_err(poem::error::InternalServerError)? as u64; + + let query = query.offset(offset).limit(limit); + + let items = query + .all(db) + .await + .map_err(poem::error::InternalServerError)?; + + let items = items.into_iter().map(postprocess).collect::>(); + Ok(PaginatedResponse { + items, + offset, + total, + }) + } +} diff --git a/warpgate-admin/src/api/sessions_list.rs b/warpgate-admin/src/api/sessions_list.rs index 2a97a76..b23e984 100644 --- a/warpgate-admin/src/api/sessions_list.rs +++ b/warpgate-admin/src/api/sessions_list.rs @@ -1,7 +1,13 @@ +use super::pagination::{PaginatedResponse, PaginationParams}; +use futures::{SinkExt, StreamExt}; +use poem::session::Session; +use poem::web::websocket::{Message, WebSocket}; use poem::web::Data; +use poem::{handler, IntoResponse}; +use poem_openapi::param::Query; use poem_openapi::payload::Json; use poem_openapi::{ApiResponse, OpenApi}; -use sea_orm::{DatabaseConnection, EntityTrait, QueryOrder}; +use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QueryOrder}; use std::sync::Arc; use tokio::sync::Mutex; use warpgate_common::{SessionSnapshot, State}; @@ -11,7 +17,7 @@ pub struct Api; #[derive(ApiResponse)] enum GetSessionsResponse { #[oai(status = 200)] - Ok(Json>), + Ok(Json>), } #[derive(ApiResponse)] @@ -26,20 +32,35 @@ impl Api { async fn api_get_all_sessions( &self, db: Data<&Arc>>, + offset: Query>, + limit: Query>, + active_only: Query>, + logged_in_only: Query>, ) -> poem::Result { use warpgate_db_entities::Session; let db = db.lock().await; - let sessions = Session::Entity::find() - .order_by_desc(Session::Column::Started) - .all(&*db) - .await - .map_err(poem::error::InternalServerError)?; - let sessions = sessions - .into_iter() - .map(Into::into) - .collect::>(); - Ok(GetSessionsResponse::Ok(Json(sessions))) + let mut q = Session::Entity::find().order_by_desc(Session::Column::Started); + + if active_only.unwrap_or(false) { + q = q.filter(Session::Column::Ended.is_null()); + } + if logged_in_only.unwrap_or(false) { + q = q.filter(Session::Column::Username.is_not_null()); + } + + Ok(GetSessionsResponse::Ok(Json( + PaginatedResponse::new( + q, + PaginationParams { + limit: *limit, + offset: *offset, + }, + &*db, + Into::into, + ) + .await?, + ))) } #[oai( @@ -50,6 +71,7 @@ impl Api { async fn api_close_all_sessions( &self, state: Data<&Arc>>, + session: &Session, ) -> poem::Result { let state = state.lock().await; @@ -58,6 +80,26 @@ impl Api { session.handle.close(); } + session.purge(); + Ok(CloseAllSessionsResponse::Ok) } } + +#[handler] +pub async fn api_get_sessions_changes_stream( + ws: WebSocket, + state: Data<&Arc>>, +) -> impl IntoResponse { + let mut receiver = state.lock().await.subscribe(); + + ws.on_upgrade(|socket| async move { + let (mut sink, _) = socket.split(); + + while receiver.recv().await.is_ok() { + sink.send(Message::Text("".to_string())).await?; + } + + Ok::<(), anyhow::Error>(()) + }) +} diff --git a/warpgate-admin/src/api/ssh_keys.rs b/warpgate-admin/src/api/ssh_keys.rs index 102d3c1..62bd7c3 100644 --- a/warpgate-admin/src/api/ssh_keys.rs +++ b/warpgate-admin/src/api/ssh_keys.rs @@ -40,7 +40,7 @@ impl Api { .into_iter() .map(|k| SSHKey { kind: k.name().to_owned(), - public_key_base64: k.public_key_base64().replace('\n', "").replace('\r', ""), + public_key_base64: k.public_key_base64(), }) .collect(); Ok(GetSSHOwnKeysResponse::Ok(Json(keys))) diff --git a/warpgate-admin/src/lib.rs b/warpgate-admin/src/lib.rs index ce1ec47..dfa85fe 100644 --- a/warpgate-admin/src/lib.rs +++ b/warpgate-admin/src/lib.rs @@ -36,6 +36,10 @@ pub fn admin_api_app(services: &Services) -> impl IntoEndpoint { "/recordings/:id/tcpdump", crate::api::recordings_detail::api_get_recording_tcpdump, ) + .at( + "/sessions/changes", + crate::api::sessions_list::api_get_sessions_changes_stream, + ) .data(db) .data(config_provider) .data(state) diff --git a/warpgate-common/src/config_providers/file.rs b/warpgate-common/src/config_providers/file.rs index 3a7654d..c02a54c 100644 --- a/warpgate-common/src/config_providers/file.rs +++ b/warpgate-common/src/config_providers/file.rs @@ -102,7 +102,6 @@ impl ConfigProvider for FileConfigProvider { let mut base64_bytes = BASE64_MIME.encode(public_key_bytes); base64_bytes.pop(); base64_bytes.pop(); - let base64_bytes = base64_bytes.replace("\r\n", ""); let client_key = format!("{} {}", kind, base64_bytes); debug!(username = &user.username[..], "Client key: {}", client_key); diff --git a/warpgate-common/src/lib.rs b/warpgate-common/src/lib.rs index b5180b5..457a418 100644 --- a/warpgate-common/src/lib.rs +++ b/warpgate-common/src/lib.rs @@ -20,6 +20,6 @@ pub use config_providers::*; pub use data::*; pub use protocols::*; pub use services::*; -pub use state::{SessionState, State}; +pub use state::{SessionState, SessionStateInit, State}; pub use try_macro::*; pub use types::*; diff --git a/warpgate-common/src/protocols/handle.rs b/warpgate-common/src/protocols/handle.rs index df20f51..9a9a9b1 100644 --- a/warpgate-common/src/protocols/handle.rs +++ b/warpgate-common/src/protocols/handle.rs @@ -43,7 +43,9 @@ impl WarpgateServerHandle { use sea_orm::ActiveValue::Set; { - self.session_state.lock().await.username = Some(username.clone()) + let mut state = self.session_state.lock().await; + state.username = Some(username.clone()); + state.emit_change() } let db = self.db.lock().await; @@ -63,7 +65,9 @@ impl WarpgateServerHandle { pub async fn set_target(&self, target: &Target) -> Result<()> { use sea_orm::ActiveValue::Set; { - self.session_state.lock().await.target = Some(target.clone()); + let mut state = self.session_state.lock().await; + state.target = Some(target.clone()); + state.emit_change() } let db = self.db.lock().await; diff --git a/warpgate-common/src/state.rs b/warpgate-common/src/state.rs index 4f671bc..8435a2e 100644 --- a/warpgate-common/src/state.rs +++ b/warpgate-common/src/state.rs @@ -4,7 +4,7 @@ use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait}; use std::collections::HashMap; use std::net::SocketAddr; use std::sync::{Arc, Weak}; -use tokio::sync::Mutex; +use tokio::sync::{broadcast, Mutex}; use tracing::*; use uuid::Uuid; use warpgate_db_entities::Session; @@ -13,15 +13,18 @@ pub struct State { pub sessions: HashMap>>, db: Arc>, this: Weak>, + change_sender: broadcast::Sender<()>, } impl State { pub fn new(db: &Arc>) -> Arc> { + let sender = broadcast::channel(2).0; Arc::>::new_cyclic(|me| { Mutex::new(Self { sessions: HashMap::new(), db: db.clone(), this: me.clone(), + change_sender: sender, }) }) } @@ -29,10 +32,16 @@ impl State { pub async fn register_session( &mut self, protocol: &ProtocolName, - session: &Arc>, + state: SessionStateInit, ) -> Result>> { let id = uuid::Uuid::new_v4(); - self.sessions.insert(id, session.clone()); + + let state = Arc::new(Mutex::new(SessionState::new( + state, + self.change_sender.clone(), + ))); + + self.sessions.insert(id, state.clone()); { use sea_orm::ActiveValue::Set; @@ -40,7 +49,7 @@ impl State { let values = Session::ActiveModel { id: Set(id), started: Set(chrono::Utc::now()), - remote_address: Set(session + remote_address: Set(state .lock() .await .remote_address @@ -57,23 +66,31 @@ impl State { .context("Error inserting session")?; } + let _ = self.change_sender.send(()); + match self.this.upgrade() { Some(this) => Ok(Arc::new(Mutex::new(WarpgateServerHandle::new( id, self.db.clone(), this, - session.clone(), + state, )))), None => anyhow::bail!("State is being detroyed"), } } + pub fn subscribe(&mut self) -> broadcast::Receiver<()> { + self.change_sender.subscribe() + } + pub async fn remove_session(&mut self, id: SessionId) { self.sessions.remove(&id); if let Err(error) = self.mark_session_complete(id).await { error!(%error, %id, "Could not update session in the DB"); } + + let _ = self.change_sender.send(()); } async fn mark_session_complete(&mut self, id: Uuid) -> Result<()> { @@ -95,15 +112,26 @@ pub struct SessionState { pub username: Option, pub target: Option, pub handle: Box, + change_sender: broadcast::Sender<()>, +} + +pub struct SessionStateInit { + pub remote_address: Option, + pub handle: Box, } impl SessionState { - pub fn new(remote_address: Option, handle: Box) -> Self { + fn new(init: SessionStateInit, change_sender: broadcast::Sender<()>) -> Self { SessionState { - remote_address, + remote_address: init.remote_address, username: None, target: None, - handle, + handle: init.handle, + change_sender, } } + + pub fn emit_change(&self) { + let _ = self.change_sender.send(()); + } } diff --git a/warpgate-protocol-http/src/common.rs b/warpgate-protocol-http/src/common.rs index 440c311..f31c9a0 100644 --- a/warpgate-protocol-http/src/common.rs +++ b/warpgate-protocol-http/src/common.rs @@ -1,15 +1,16 @@ -use std::time::Duration; - use http::StatusCode; use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; use poem::session::Session; use poem::web::{Data, Redirect}; use poem::{Endpoint, EndpointExt, FromRequest, IntoResponse, Request, Response}; -use warpgate_common::{Services, TargetOptions}; +use std::time::Duration; +use warpgate_common::{ProtocolName, Services, TargetOptions}; +pub const PROTOCOL_NAME: ProtocolName = "HTTP"; static USERNAME_SESSION_KEY: &str = "username"; static TARGET_SESSION_KEY: &str = "target_name"; pub static SESSION_MAX_AGE: Duration = Duration::from_secs(60 * 30); +pub static COOKIE_MAX_AGE: Duration = Duration::from_secs(60 * 60 * 24); pub trait SessionExt { fn has_selected_target(&self) -> bool; diff --git a/warpgate-protocol-http/src/lib.rs b/warpgate-protocol-http/src/lib.rs index b533e1b..95b29fa 100644 --- a/warpgate-protocol-http/src/lib.rs +++ b/warpgate-protocol-http/src/lib.rs @@ -6,11 +6,12 @@ mod logging; mod proxy; mod session; mod session_handle; -use crate::common::{endpoint_admin_auth, endpoint_auth, page_auth, SESSION_MAX_AGE}; +use crate::common::{endpoint_admin_auth, endpoint_auth, page_auth, COOKIE_MAX_AGE}; use crate::session::{SessionMiddleware, SharedSessionStorage}; use anyhow::{Context, Result}; use async_trait::async_trait; use common::page_admin_auth; +pub use common::PROTOCOL_NAME; use logging::{log_request_result, span_for_request}; use poem::endpoint::{EmbeddedFileEndpoint, EmbeddedFilesEndpoint}; use poem::listener::{Listener, RustlsCertificate, RustlsConfig, TcpListener}; @@ -26,11 +27,9 @@ use std::time::Duration; use tokio::sync::Mutex; use tracing::*; use warpgate_admin::admin_api_app; -use warpgate_common::{ProtocolName, ProtocolServer, Services, Target, TargetTestError}; +use warpgate_common::{ProtocolServer, Services, Target, TargetTestError}; use warpgate_web::Assets; -pub const PROTOCOL_NAME: ProtocolName = "HTTP"; - pub struct HTTPProtocolServer { services: Services, } @@ -109,7 +108,7 @@ impl ProtocolServer for HTTPProtocolServer { .with(ServerSession::new( CookieConfig::default() .secure(false) - .max_age(SESSION_MAX_AGE) + .max_age(COOKIE_MAX_AGE) .name("warpgate-http-session"), session_storage.clone(), )) diff --git a/warpgate-protocol-http/src/session.rs b/warpgate-protocol-http/src/session.rs index 2062bf3..9d25b8f 100644 --- a/warpgate-protocol-http/src/session.rs +++ b/warpgate-protocol-http/src/session.rs @@ -1,4 +1,4 @@ -use crate::common::SESSION_MAX_AGE; +use crate::common::{PROTOCOL_NAME, SESSION_MAX_AGE}; use crate::session_handle::{ HttpSessionHandle, SessionHandleCommand, WarpgateServerHandleFromRequest, }; @@ -10,7 +10,8 @@ use std::collections::{BTreeMap, HashMap}; use std::sync::{Arc, Weak}; use std::time::{Duration, Instant}; use tokio::sync::Mutex; -use warpgate_common::{Services, SessionId, SessionState, WarpgateServerHandle}; +use tracing::*; +use warpgate_common::{Services, SessionId, WarpgateServerHandle, SessionStateInit}; #[derive(Clone)] pub struct SharedSessionStorage(pub Arc>>); @@ -59,7 +60,7 @@ pub struct SessionMiddleware { } static SESSION_ID_SESSION_KEY: &str = "session_id"; -static SESSION_ID_REQUEST_COUNTER: &str = "request_counter"; +static REQUEST_COUNTER_SESSION_KEY: &str = "request_counter"; impl SessionMiddleware { pub fn new() -> Arc> { @@ -75,14 +76,14 @@ impl SessionMiddleware { pub async fn process_request(&mut self, req: Request) -> poem::Result { let session: &Session = <_>::from_request_without_body(&req).await?; - let request_counter = session.get::(SESSION_ID_REQUEST_COUNTER).unwrap_or(0); - session.set(SESSION_ID_REQUEST_COUNTER, request_counter + 1); + let request_counter = session.get::(REQUEST_COUNTER_SESSION_KEY).unwrap_or(0); + session.set(REQUEST_COUNTER_SESSION_KEY, request_counter + 1); if let Some(session_id) = session.get::(SESSION_ID_SESSION_KEY) { self.session_timestamps.insert(session_id, Instant::now()); - } else if request_counter == 5 { + // } else if request_counter == 5 { // Start logging sessions when they've got 5 requests - self.create_handle_for(&req).await?; + // self.create_handle_for(&req).await?; }; Ok(req) @@ -104,16 +105,18 @@ impl SessionMiddleware { Data::<&SharedSessionStorage>::from_request_without_body(&req).await?; let (session_handle, mut session_handle_rx) = HttpSessionHandle::new(); - let session_state = Arc::new(Mutex::new(SessionState::new( - remote_address.0.as_socket_addr().cloned(), - Box::new(session_handle), - ))); let server_handle = services .state .lock() .await - .register_session(&crate::PROTOCOL_NAME, &session_state) + .register_session( + &PROTOCOL_NAME, + SessionStateInit { + remote_address: remote_address.0.as_socket_addr().cloned(), + handle: Box::new(session_handle), + }, + ) .await?; let id = server_handle.lock().await.id(); @@ -133,7 +136,10 @@ impl SessionMiddleware { if let Some(ref poem_session_id) = poem_session_id { let _ = session_storage.remove_session(&poem_session_id).await; } - this.lock().await.session_handles.remove(&id); + info!(%id, "Removed HTTP session"); + let mut that = this.lock().await; + that.session_handles.remove(&id); + that.session_timestamps.remove(&id); } } } diff --git a/warpgate-protocol-ssh/src/helpers.rs b/warpgate-protocol-ssh/src/helpers.rs index 0104098..e6b4c11 100644 --- a/warpgate-protocol-ssh/src/helpers.rs +++ b/warpgate-protocol-ssh/src/helpers.rs @@ -10,7 +10,7 @@ impl PublicKeyAsOpenSSH for KeyPair { let mut buf = String::new(); buf.push_str(self.name()); buf.push(' '); - buf.push_str(&self.public_key_base64().replace("\r\n", "")); + buf.push_str(&self.public_key_base64()); buf } } diff --git a/warpgate-protocol-ssh/src/server/mod.rs b/warpgate-protocol-ssh/src/server/mod.rs index 4ac7b78..e1c7e35 100644 --- a/warpgate-protocol-ssh/src/server/mod.rs +++ b/warpgate-protocol-ssh/src/server/mod.rs @@ -13,9 +13,8 @@ use std::net::SocketAddr; use std::sync::Arc; use tokio::io::{AsyncRead, AsyncWrite}; use tokio::net::TcpListener; -use tokio::sync::Mutex; use tracing::*; -use warpgate_common::{Services, SessionState}; +use warpgate_common::{Services, SessionStateInit}; pub async fn run_server(services: Services, address: SocketAddr) -> Result<()> { let russh_config = { @@ -36,16 +35,18 @@ pub async fn run_server(services: Services, address: SocketAddr) -> Result<()> { let russh_config = russh_config.clone(); let (session_handle, session_handle_rx) = SSHSessionHandle::new(); - let session_state = Arc::new(Mutex::new(SessionState::new( - Some(remote_address), - Box::new(session_handle), - ))); let server_handle = services .state .lock() .await - .register_session(&crate::PROTOCOL_NAME, &session_state) + .register_session( + &crate::PROTOCOL_NAME, + SessionStateInit { + remote_address: Some(remote_address), + handle: Box::new(session_handle), + }, + ) .await?; let id = server_handle.lock().await.id(); diff --git a/warpgate-protocol-ssh/src/server/session.rs b/warpgate-protocol-ssh/src/server/session.rs index 2847600..96dec85 100644 --- a/warpgate-protocol-ssh/src/server/session.rs +++ b/warpgate-protocol-ssh/src/server/session.rs @@ -747,9 +747,10 @@ impl ServerSession { async fn traffic_recorder_for( &mut self, - host: &String, + host: &str, port: u32, ) -> Option<&mut TrafficRecorder> { + let host = host.to_owned(); if let Vacant(e) = self.traffic_recorders.entry((host.clone(), port)) { match self .services @@ -767,7 +768,7 @@ impl ServerSession { } } } - self.traffic_recorders.get_mut(&(host.clone(), port)) + self.traffic_recorders.get_mut(&(host, port)) } pub async fn _channel_shell_request( diff --git a/warpgate-web/package.json b/warpgate-web/package.json index 1eca69d..c71504a 100644 --- a/warpgate-web/package.json +++ b/warpgate-web/package.json @@ -27,6 +27,7 @@ "@typescript-eslint/eslint-plugin": "^5.28.0", "@typescript-eslint/parser": "^5.28.0", "bootstrap": "^5.2.0-beta1", + "copy-text-to-clipboard": "^3.0.1", "eslint": "^8.18.0", "eslint-config-standard": "^16.0.3", "eslint-import-resolver-typescript": "^2.7.1", @@ -41,6 +42,7 @@ "svelte-check": "^2.7.2", "svelte-fa": "^3.0.1", "svelte-intersection-observer": "^0.10.0", + "svelte-observable": "^0.4.0", "svelte-preprocess": "^4.10.7", "svelte-spa-router": "^3.2.0", "sveltestrap": "^5.9.0", diff --git a/warpgate-web/src/admin/App.svelte b/warpgate-web/src/admin/App.svelte index 1ca5a77..ca9dde6 100644 --- a/warpgate-web/src/admin/App.svelte +++ b/warpgate-web/src/admin/App.svelte @@ -71,7 +71,6 @@ const routes = {
{$serverInfo?.username}
- diff --git a/warpgate-web/src/admin/Home.svelte b/warpgate-web/src/admin/Home.svelte index e43cc5c..c48e0cc 100644 --- a/warpgate-web/src/admin/Home.svelte +++ b/warpgate-web/src/admin/Home.svelte @@ -1,20 +1,48 @@ -{#if !$sessions} - -{:else} -
- {#if $activeSessions } -

Sessions right now: {$activeSessions}

-
- - Close all sessions - -
- {:else} -

No active sessions

- {/if} +
+ {#if activeSessionCount } +

Sessions right now: {activeSessionCount}

+
+ + Close all sessions + +
+ {:else} +

No active sessions

+ {/if} +
+ + +
+
+ +
- {#if $sortedSessions } -
- {#each $sortedSessions as session} - - - {/if} -{/if} + +