diff --git a/Cargo.lock b/Cargo.lock index 9450f74..cb22a4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -350,10 +350,12 @@ dependencies = [ "anyhow", "async-nats", "axum", + "base64 0.22.1", "chrono", "console_error_panic_hook", "dotenvy", "futures", + "js-sys", "leptos", "leptos_axum", "leptos_meta", @@ -371,6 +373,7 @@ dependencies = [ "url", "uuid", "wasm-bindgen", + "wasm-bindgen-futures", "web-sys", ] diff --git a/Cargo.toml b/Cargo.toml index c4739aa..da7a6f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,12 @@ sqlx = { version = "0.8", default-features = false, features = [ "macros", ], optional = true } openidconnect = { version = "4", optional = true } +# For pulling the `groups` custom claim out of the already-verified ID +# token's raw JWT payload - openidconnect's Core* type aliases default to +# EmptyAdditionalClaims, and reworking that generic stack for one extra +# field isn't worth it. The token's signature is already checked by +# id_token.claims(...) before this ever runs. +base64 = { version = "0.22", optional = true } futures = { version = "0.3", optional = true } chrono = { version = "0.4", features = ["serde"], optional = true } uuid = { version = "1", features = ["v4"], optional = true } @@ -39,12 +45,33 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"], optional = tr # --- browser only --- wasm-bindgen = { version = "0.2", optional = true } +wasm-bindgen-futures = { version = "0.4", optional = true } +js-sys = { version = "0.3", optional = true } console_error_panic_hook = { version = "0.1", optional = true } web-sys = { version = "0.3", features = [ "EventSource", "MessageEvent", "HtmlElement", "Element", + # --- WebRTC mesh calling --- + "RtcPeerConnection", + "RtcConfiguration", + "RtcIceServer", + "RtcSdpType", + "RtcSessionDescriptionInit", + "RtcIceCandidate", + "RtcIceCandidateInit", + "RtcPeerConnectionIceEvent", + "RtcRtpSender", + "RtcTrackEvent", + "RtcRtpTransceiver", + "RtcOfferOptions", + "MediaStream", + "MediaStreamConstraints", + "MediaStreamTrack", + "MediaDevices", + "Navigator", + "HtmlVideoElement", ], optional = true } [features] @@ -52,6 +79,8 @@ default = [] hydrate = [ "leptos/hydrate", "dep:wasm-bindgen", + "dep:wasm-bindgen-futures", + "dep:js-sys", "dep:console_error_panic_hook", "dep:web-sys", ] @@ -69,6 +98,7 @@ ssr = [ "dep:url", "dep:sqlx", "dep:openidconnect", + "dep:base64", "dep:futures", "dep:chrono", "dep:uuid", diff --git a/src/app.rs b/src/app.rs index d489cd8..f70846e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -7,7 +7,10 @@ use leptos_router::{ }; use crate::auth::{current_user, User}; -use crate::chat::{is_valid_room, room_subject, ChatMessage, SendMessage, DEFAULT_ROOM, ROOMS}; +use crate::chat::{ + is_authorized_for_room, is_valid_room, room_subject, ChatMessage, SendMessage, DEFAULT_ROOM, + ROOMS, +}; #[cfg(feature = "hydrate")] use crate::chat::room_history; @@ -75,7 +78,23 @@ fn ChatPage() -> impl IntoView { {move || { user.get() .map(|res| match res { - Ok(Some(u)) => view! { }.into_any(), + Ok(Some(u)) => { + // `room`'s own Memo only knows about is_valid_room (no + // user context available that early) - fall back to + // DEFAULT_ROOM here too if this user isn't authorized + // for the room the URL asked for, same as an unknown + // room name already does. + let u2 = u.clone(); + let effective_room = Memo::new(move |_| { + let r = room.get(); + if is_authorized_for_room(&u2, &r) { + r + } else { + DEFAULT_ROOM.to_string() + } + }); + view! { }.into_any() + } _ => view! { }.into_any(), }) }} @@ -83,6 +102,28 @@ fn ChatPage() -> impl IntoView { } } +// --------------------------------------------------------------------------- +// brand mark - the "voice pulse" ornament, currentColor so it always +// matches whatever text color surrounds it (sidebar wordmark vs. the much +// larger gate title) +// --------------------------------------------------------------------------- + +#[component] +fn PulseMark() -> impl IntoView { + view! { + + + + } +} + // --------------------------------------------------------------------------- // unauthenticated: the gate // --------------------------------------------------------------------------- @@ -94,8 +135,10 @@ fn LoginGate() -> impl IntoView {
"MESSAGE BUS · AUTH REQUIRED"

+ "CN" "ATS"

+

"chat over the bus"

"Realtime chat carried on NATS subjects. Identity issued by your Kanidm realm — no separate passwords, no local accounts."

@@ -210,11 +253,13 @@ fn ChatShell(room: Memo, user: User) -> impl IntoView { }; let me = user.username.clone(); + let call_me = me.clone(); view! {