Add logout functionallity
This commit is contained in:
parent
8a8e773795
commit
c94105acc3
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -550,6 +550,7 @@ dependencies = [
|
|||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"gloo 0.11.0",
|
"gloo 0.11.0",
|
||||||
"log",
|
"log",
|
||||||
|
"serde",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-logger",
|
"wasm-logger",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
|
@ -6,3 +6,8 @@ pub struct ChatMessage {
|
|||||||
pub message: String,
|
pub message: String,
|
||||||
pub time: DateTime<Local>,
|
pub time: DateTime<Local>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct LoggedInResponse {
|
||||||
|
pub logged_in: bool,
|
||||||
|
}
|
||||||
|
@ -10,6 +10,7 @@ common = { version = "0.1.0", path = "../common" }
|
|||||||
console_error_panic_hook = "0.1.7"
|
console_error_panic_hook = "0.1.7"
|
||||||
gloo = "0.11.0"
|
gloo = "0.11.0"
|
||||||
log = "0.4.22"
|
log = "0.4.22"
|
||||||
|
serde = { version = "1.0.210", features = ["derive"] }
|
||||||
wasm-bindgen = "0.2.93"
|
wasm-bindgen = "0.2.93"
|
||||||
wasm-logger = "0.2.0"
|
wasm-logger = "0.2.0"
|
||||||
web-sys = { version = "0.3.70", features = ["Navigator", "WebSocket", "EventListener"] }
|
web-sys = { version = "0.3.70", features = ["Navigator", "WebSocket", "EventListener"] }
|
||||||
|
@ -1,12 +1,63 @@
|
|||||||
use yew::prelude::*;
|
use common::LoggedInResponse;
|
||||||
|
use gloo::net::http::Request;
|
||||||
|
use yew::{platform::spawn_local, prelude::*};
|
||||||
|
use yew_hooks::{use_async_with_options, UseAsyncOptions};
|
||||||
|
use yew_router::hooks::use_navigator;
|
||||||
|
|
||||||
|
use crate::Route;
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
pub fn Nav() -> Html {
|
pub fn Nav() -> Html {
|
||||||
|
let navigator = use_navigator();
|
||||||
|
let logged_in = use_async_with_options(
|
||||||
|
async move {
|
||||||
|
let location = web_sys::window().unwrap().location();
|
||||||
|
let logged_in_url = format!(
|
||||||
|
"{}//{}/api/logged_in",
|
||||||
|
location.protocol().unwrap(),
|
||||||
|
location.host().unwrap()
|
||||||
|
);
|
||||||
|
let resp = Request::get(&logged_in_url)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.json::<LoggedInResponse>()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
Result::<bool, ()>::Ok(resp.logged_in)
|
||||||
|
},
|
||||||
|
UseAsyncOptions::enable_auto(),
|
||||||
|
);
|
||||||
|
let logout = {
|
||||||
|
let logged_in = logged_in.clone();
|
||||||
|
Callback::from(move |_| {
|
||||||
|
let navigator = navigator.clone();
|
||||||
|
let logged_in = logged_in.clone();
|
||||||
|
//let update = update.clone();
|
||||||
|
spawn_local(async move {
|
||||||
|
let location = web_sys::window().unwrap().location();
|
||||||
|
let logout_url = format!(
|
||||||
|
"{}//{}/api/logout",
|
||||||
|
location.protocol().unwrap(),
|
||||||
|
location.host().unwrap()
|
||||||
|
);
|
||||||
|
Request::post(&logout_url)
|
||||||
|
.body("")
|
||||||
|
.unwrap()
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
logged_in.run();
|
||||||
|
navigator.as_ref().unwrap().push(&Route::Home);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
};
|
||||||
html! {
|
html! {
|
||||||
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a class="navbar-brand" href="#">{ "Chat" }</a>
|
<a class="navbar-brand" href="/">{ "Chat" }</a>
|
||||||
<div class="nav-item dropdown navbar-nav">
|
<div class="nav-item dropdown navbar-nav">
|
||||||
|
if logged_in.data.unwrap_or(false) {
|
||||||
<a
|
<a
|
||||||
class="nav-link dropdown-toggle"
|
class="nav-link dropdown-toggle"
|
||||||
href="#"
|
href="#"
|
||||||
@ -16,12 +67,23 @@ pub fn Nav() -> Html {
|
|||||||
>
|
>
|
||||||
{ "PJHT" }
|
{ "PJHT" }
|
||||||
</a>
|
</a>
|
||||||
|
} else {
|
||||||
|
<a
|
||||||
|
class="nav-link dropdown-toggle disabled"
|
||||||
|
href="#"
|
||||||
|
role="button"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
{ "Not logged in" }
|
||||||
|
</a>
|
||||||
|
}
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="#">{ "Options" }</a>
|
<a class="dropdown-item" href="#">{ "Options" }</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="#">{ "Logout" }</a>
|
<a class="dropdown-item" href="#" onclick={logout}>{ "Logout" }</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
pub mod components;
|
pub mod components;
|
||||||
mod pages;
|
mod pages;
|
||||||
use components::Nav;
|
use components::Nav;
|
||||||
use pages::{Home, Login};
|
use pages::{ChatPage, Home, Login};
|
||||||
|
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
use yew_router::prelude::*;
|
use yew_router::prelude::*;
|
||||||
@ -10,6 +10,8 @@ use yew_router::prelude::*;
|
|||||||
enum Route {
|
enum Route {
|
||||||
#[at("/")]
|
#[at("/")]
|
||||||
Home,
|
Home,
|
||||||
|
#[at("/chat")]
|
||||||
|
ChatPage,
|
||||||
#[at("/login")]
|
#[at("/login")]
|
||||||
Login,
|
Login,
|
||||||
#[not_found]
|
#[not_found]
|
||||||
@ -20,6 +22,7 @@ enum Route {
|
|||||||
fn switch(routes: Route) -> Html {
|
fn switch(routes: Route) -> Html {
|
||||||
match routes {
|
match routes {
|
||||||
Route::Home => html! { <Home /> },
|
Route::Home => html! { <Home /> },
|
||||||
|
Route::ChatPage => html! { <ChatPage /> },
|
||||||
Route::Login => html! { <Login /> },
|
Route::Login => html! { <Login /> },
|
||||||
Route::NotFound => html! {
|
Route::NotFound => html! {
|
||||||
<>
|
<>
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
mod home;
|
mod home;
|
||||||
pub use home::Home;
|
pub use home::Home;
|
||||||
|
mod chat;
|
||||||
|
pub use chat::ChatPage;
|
||||||
mod login;
|
mod login;
|
||||||
pub use login::Login;
|
pub use login::{Login, LoginQuery};
|
||||||
|
238
frontend/src/pages/chat.rs
Normal file
238
frontend/src/pages/chat.rs
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use chrono::Local;
|
||||||
|
use common::ChatMessage;
|
||||||
|
use gloo::{console::log, events::EventListener};
|
||||||
|
use wasm_bindgen::{JsCast, JsValue};
|
||||||
|
use web_sys::{js_sys::Uint8Array, CloseEvent, Event, MessageEvent, WebSocket};
|
||||||
|
use yew::{platform::time::sleep, prelude::*};
|
||||||
|
use yew_router::prelude::RouterScopeExt;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
components::{Alert, Message, Nav},
|
||||||
|
pages::LoginQuery,
|
||||||
|
Route,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub enum ChatPageMessage {
|
||||||
|
SubmittedMessage(String),
|
||||||
|
RecievedMessage(ChatMessage),
|
||||||
|
WsClose(CloseEvent),
|
||||||
|
WsOpen,
|
||||||
|
WsReconnect,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq)]
|
||||||
|
enum WsState {
|
||||||
|
Closed,
|
||||||
|
Open,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ChatPage {
|
||||||
|
messages: Vec<ChatMessage>,
|
||||||
|
chat_ws: Option<WebSocketConn>,
|
||||||
|
ws_state: WsState,
|
||||||
|
ws_reconnecting: bool,
|
||||||
|
message_container_ref: NodeRef,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WebSocketConn {
|
||||||
|
ws: WebSocket,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
listeners: [EventListener; 3],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebSocketConn {
|
||||||
|
fn send_binary(&self, msg: &[u8]) -> Result<(), JsValue> {
|
||||||
|
self.ws.send_with_u8_array(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for WebSocketConn {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = self.ws.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChatPage {
|
||||||
|
fn connect_ws(&mut self, ctx: &Context<Self>) {
|
||||||
|
let location = web_sys::window().unwrap().location();
|
||||||
|
let ws_proto = if location.protocol().unwrap() == "https:" {
|
||||||
|
"wss"
|
||||||
|
} else {
|
||||||
|
"ws"
|
||||||
|
};
|
||||||
|
let ws_url = format!("{}://{}/api/chat_ws", ws_proto, location.host().unwrap());
|
||||||
|
log!("Connecting to ", &ws_url);
|
||||||
|
let ws = WebSocket::new(&ws_url).unwrap();
|
||||||
|
ws.set_binary_type(web_sys::BinaryType::Arraybuffer);
|
||||||
|
let link = ctx.link().clone();
|
||||||
|
let open_ev = EventListener::new(&ws, "open", move |_event| {
|
||||||
|
link.send_message(ChatPageMessage::WsOpen);
|
||||||
|
});
|
||||||
|
let link = ctx.link().clone();
|
||||||
|
let close_ev = EventListener::new(&ws, "close", move |event: &Event| {
|
||||||
|
let event = event.dyn_ref::<CloseEvent>().unwrap();
|
||||||
|
link.send_message(ChatPageMessage::WsClose(event.clone()));
|
||||||
|
});
|
||||||
|
let link = ctx.link().clone();
|
||||||
|
let msg_ev = EventListener::new(&ws, "message", move |event: &Event| {
|
||||||
|
let event = event.dyn_ref::<MessageEvent>().unwrap();
|
||||||
|
let bytes = event.data();
|
||||||
|
let bytes = Uint8Array::new(&bytes).to_vec();
|
||||||
|
let msg = ciborium::from_reader(bytes.as_slice()).unwrap();
|
||||||
|
link.send_message(ChatPageMessage::RecievedMessage(msg));
|
||||||
|
});
|
||||||
|
self.chat_ws = Some(WebSocketConn {
|
||||||
|
ws,
|
||||||
|
listeners: [open_ev, close_ev, msg_ev],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for ChatPage {
|
||||||
|
type Message = ChatPageMessage;
|
||||||
|
|
||||||
|
type Properties = ();
|
||||||
|
|
||||||
|
fn create(ctx: &Context<Self>) -> Self {
|
||||||
|
let mut slf = Self {
|
||||||
|
messages: Vec::new(),
|
||||||
|
chat_ws: None,
|
||||||
|
ws_state: WsState::Closed,
|
||||||
|
ws_reconnecting: false,
|
||||||
|
message_container_ref: NodeRef::default(),
|
||||||
|
};
|
||||||
|
slf.connect_ws(ctx);
|
||||||
|
slf
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
|
let msg_keypress = {
|
||||||
|
ctx.link().batch_callback(|press: KeyboardEvent| {
|
||||||
|
if press.key() == "Enter" {
|
||||||
|
let input = press
|
||||||
|
.target_dyn_into::<web_sys::HtmlInputElement>()
|
||||||
|
.unwrap();
|
||||||
|
let msg = input.value();
|
||||||
|
input.set_value("");
|
||||||
|
//if on_mobile() {
|
||||||
|
// input.blur().unwrap();
|
||||||
|
//}
|
||||||
|
Some(ChatPageMessage::SubmittedMessage(msg))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let disable_input = self.ws_state != WsState::Open;
|
||||||
|
html! {
|
||||||
|
<div class="myvh-100 d-flex flex-column">
|
||||||
|
<Nav />
|
||||||
|
<div class="container-fluid d-flex flex-column flex-grow-1 mt-3">
|
||||||
|
if self.ws_state != WsState::Open {
|
||||||
|
<Alert message="Connection to backend lost, trying to reconnect" />
|
||||||
|
}
|
||||||
|
<div
|
||||||
|
ref={self.message_container_ref.clone()}
|
||||||
|
class="d-flex border rounded flex-grow-1 flex-column-reverse overflow-auto mb-3"
|
||||||
|
style="flex-basis: 0"
|
||||||
|
>
|
||||||
|
{ self.messages.iter().rev().map(|msg| html!{
|
||||||
|
<Message message={msg.clone()}/>
|
||||||
|
}
|
||||||
|
).collect::<Vec<_>>() }
|
||||||
|
</div>
|
||||||
|
<div class="bottom-0 mb-3 mt-3">
|
||||||
|
<input
|
||||||
|
onkeydown={msg_keypress}
|
||||||
|
disabled={disable_input}
|
||||||
|
type="text"
|
||||||
|
id="message"
|
||||||
|
class="w-100 form-control"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||||
|
match msg {
|
||||||
|
ChatPageMessage::SubmittedMessage(msg) => {
|
||||||
|
let msg = ChatMessage {
|
||||||
|
message: msg.clone(),
|
||||||
|
time: Local::now(),
|
||||||
|
};
|
||||||
|
self.messages.push(msg.clone());
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
ciborium::into_writer(&msg, &mut buf).unwrap();
|
||||||
|
self.chat_ws.as_ref().unwrap().send_binary(&buf).unwrap();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
ChatPageMessage::RecievedMessage(msg) => {
|
||||||
|
self.messages.push(msg);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
ChatPageMessage::WsClose(close_event) => {
|
||||||
|
let code = close_event.code();
|
||||||
|
log!("WS connection closed with code", code);
|
||||||
|
if code == 4000 {
|
||||||
|
log!("Unauthorized, redirecting to login page");
|
||||||
|
ctx.link()
|
||||||
|
.navigator()
|
||||||
|
.unwrap()
|
||||||
|
.push_with_query(
|
||||||
|
&Route::Login,
|
||||||
|
&LoginQuery {
|
||||||
|
redirect: "/chat".to_string(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if self.ws_state == WsState::Closed {
|
||||||
|
log!("Already closed");
|
||||||
|
if !self.ws_reconnecting {
|
||||||
|
log!("Reconnecting in 5s");
|
||||||
|
self.ws_reconnecting = true;
|
||||||
|
ctx.link().send_future(async move {
|
||||||
|
sleep(Duration::from_secs(5)).await;
|
||||||
|
ChatPageMessage::WsReconnect
|
||||||
|
});
|
||||||
|
}
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
log!("Reconnecting");
|
||||||
|
self.connect_ws(ctx);
|
||||||
|
self.ws_state = WsState::Closed;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ChatPageMessage::WsOpen => {
|
||||||
|
if self.ws_state != WsState::Open {
|
||||||
|
log!("WS connection opened");
|
||||||
|
self.messages.clear();
|
||||||
|
self.ws_state = WsState::Open;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ChatPageMessage::WsReconnect => {
|
||||||
|
if self.ws_state != WsState::Open {
|
||||||
|
log!("Reconnecting");
|
||||||
|
self.ws_reconnecting = false;
|
||||||
|
self.connect_ws(ctx);
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
|
||||||
|
if let Some(message_container) = self.message_container_ref.cast::<web_sys::Element>() {
|
||||||
|
message_container.set_scroll_top(message_container.scroll_height());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,228 +1,72 @@
|
|||||||
use std::time::Duration;
|
use crate::{components::Nav, pages::LoginQuery, Route};
|
||||||
|
use common::LoggedInResponse;
|
||||||
|
use gloo::net::http::Request;
|
||||||
|
use yew::prelude::*;
|
||||||
|
use yew_hooks::{use_async_with_options, UseAsyncOptions};
|
||||||
|
use yew_router::hooks::use_navigator;
|
||||||
|
|
||||||
use chrono::Local;
|
#[function_component]
|
||||||
use common::ChatMessage;
|
#[allow(non_snake_case)] // Component names should be in PascalCase
|
||||||
use gloo::{console::log, events::EventListener};
|
pub fn Home() -> Html {
|
||||||
use wasm_bindgen::{JsCast, JsValue};
|
let navigator = use_navigator();
|
||||||
use web_sys::{js_sys::Uint8Array, CloseEvent, Event, MessageEvent, WebSocket};
|
let open_onclick = {
|
||||||
use yew::{platform::time::sleep, prelude::*};
|
let navigator = navigator.clone();
|
||||||
use yew_router::prelude::RouterScopeExt;
|
Callback::from(move |_| navigator.clone().unwrap().push(&Route::ChatPage))
|
||||||
|
|
||||||
use crate::{
|
|
||||||
components::{Alert, Message, Nav},
|
|
||||||
Route,
|
|
||||||
};
|
};
|
||||||
|
let login_onclick = {
|
||||||
pub enum HomeMessage {
|
let navigator = navigator.clone();
|
||||||
SubmittedMessage(String),
|
Callback::from(move |_| {
|
||||||
RecievedMessage(ChatMessage),
|
navigator
|
||||||
WsClose(CloseEvent),
|
.clone()
|
||||||
WsOpen,
|
.unwrap()
|
||||||
WsReconnect,
|
.push_with_query(
|
||||||
}
|
&Route::Login,
|
||||||
|
&LoginQuery {
|
||||||
#[derive(PartialEq, Eq)]
|
redirect: "/".to_string(),
|
||||||
enum WsState {
|
},
|
||||||
Closed,
|
)
|
||||||
Open,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Home {
|
|
||||||
messages: Vec<ChatMessage>,
|
|
||||||
chat_ws: Option<WebSocketConn>,
|
|
||||||
ws_state: WsState,
|
|
||||||
ws_reconnecting: bool,
|
|
||||||
message_container_ref: NodeRef,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct WebSocketConn {
|
|
||||||
ws: WebSocket,
|
|
||||||
#[allow(dead_code)]
|
|
||||||
listeners: [EventListener; 3],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WebSocketConn {
|
|
||||||
fn send_binary(&self, msg: &[u8]) -> Result<(), JsValue> {
|
|
||||||
self.ws.send_with_u8_array(msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for WebSocketConn {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
let _ = self.ws.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Home {
|
|
||||||
fn connect_ws(&mut self, ctx: &Context<Self>) {
|
|
||||||
let location = web_sys::window().unwrap().location();
|
|
||||||
let ws_proto = if location.protocol().unwrap() == "https:" {
|
|
||||||
"wss"
|
|
||||||
} else {
|
|
||||||
"ws"
|
|
||||||
};
|
|
||||||
let ws_url = format!("{}://{}/api/chat_ws", ws_proto, location.host().unwrap());
|
|
||||||
log!("Connecting to ", &ws_url);
|
|
||||||
let ws = WebSocket::new(&ws_url).unwrap();
|
|
||||||
ws.set_binary_type(web_sys::BinaryType::Arraybuffer);
|
|
||||||
let link = ctx.link().clone();
|
|
||||||
let open_ev = EventListener::new(&ws, "open", move |_event| {
|
|
||||||
link.send_message(HomeMessage::WsOpen);
|
|
||||||
});
|
|
||||||
let link = ctx.link().clone();
|
|
||||||
let close_ev = EventListener::new(&ws, "close", move |event: &Event| {
|
|
||||||
let event = event.dyn_ref::<CloseEvent>().unwrap();
|
|
||||||
link.send_message(HomeMessage::WsClose(event.clone()));
|
|
||||||
});
|
|
||||||
let link = ctx.link().clone();
|
|
||||||
let msg_ev = EventListener::new(&ws, "message", move |event: &Event| {
|
|
||||||
let event = event.dyn_ref::<MessageEvent>().unwrap();
|
|
||||||
let bytes = event.data();
|
|
||||||
let bytes = Uint8Array::new(&bytes).to_vec();
|
|
||||||
let msg = ciborium::from_reader(bytes.as_slice()).unwrap();
|
|
||||||
link.send_message(HomeMessage::RecievedMessage(msg));
|
|
||||||
});
|
|
||||||
self.chat_ws = Some(WebSocketConn {
|
|
||||||
ws,
|
|
||||||
listeners: [open_ev, close_ev, msg_ev],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for Home {
|
|
||||||
type Message = HomeMessage;
|
|
||||||
|
|
||||||
type Properties = ();
|
|
||||||
|
|
||||||
fn create(ctx: &Context<Self>) -> Self {
|
|
||||||
let mut slf = Self {
|
|
||||||
messages: Vec::new(),
|
|
||||||
chat_ws: None,
|
|
||||||
ws_state: WsState::Closed,
|
|
||||||
ws_reconnecting: false,
|
|
||||||
message_container_ref: NodeRef::default(),
|
|
||||||
};
|
|
||||||
slf.connect_ws(ctx);
|
|
||||||
slf
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
|
||||||
let msg_keypress = {
|
|
||||||
ctx.link().batch_callback(|press: KeyboardEvent| {
|
|
||||||
if press.key() == "Enter" {
|
|
||||||
let input = press
|
|
||||||
.target_dyn_into::<web_sys::HtmlInputElement>()
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let msg = input.value();
|
|
||||||
input.set_value("");
|
|
||||||
//if on_mobile() {
|
|
||||||
// input.blur().unwrap();
|
|
||||||
//}
|
|
||||||
Some(HomeMessage::SubmittedMessage(msg))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
let disable_input = self.ws_state != WsState::Open;
|
let logged_in = use_async_with_options(
|
||||||
|
async move {
|
||||||
|
let location = web_sys::window().unwrap().location();
|
||||||
|
let logged_in_url = format!(
|
||||||
|
"{}//{}/api/logged_in",
|
||||||
|
location.protocol().unwrap(),
|
||||||
|
location.host().unwrap()
|
||||||
|
);
|
||||||
|
let resp = Request::get(&logged_in_url)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.json::<LoggedInResponse>()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
Result::<bool, ()>::Ok(resp.logged_in)
|
||||||
|
},
|
||||||
|
UseAsyncOptions::enable_auto(),
|
||||||
|
);
|
||||||
html! {
|
html! {
|
||||||
<div class="myvh-100 d-flex flex-column">
|
<>
|
||||||
<Nav />
|
<Nav />
|
||||||
<div class="container-fluid d-flex flex-column flex-grow-1 mt-3">
|
<div class="container-fluid d-flex flex-column align-items-center">
|
||||||
if self.ws_state != WsState::Open {
|
<h1>{ "Local Chat" }</h1>
|
||||||
<Alert message="Connection to backend lost, trying to reconnect" />
|
|
||||||
}
|
|
||||||
<div
|
|
||||||
ref={self.message_container_ref.clone()}
|
|
||||||
class="d-flex border rounded flex-grow-1 flex-column-reverse overflow-auto mb-3"
|
|
||||||
style="flex-basis: 0"
|
|
||||||
>
|
|
||||||
{ self.messages.iter().rev().map(|msg| html!{
|
|
||||||
<Message message={msg.clone()}/>
|
|
||||||
}
|
|
||||||
).collect::<Vec<_>>() }
|
|
||||||
</div>
|
|
||||||
<div class="bottom-0 mb-3 mt-3">
|
|
||||||
<input
|
<input
|
||||||
onkeydown={msg_keypress}
|
type="button"
|
||||||
disabled={disable_input}
|
class="btn btn-primary"
|
||||||
type="text"
|
value="Open Chat"
|
||||||
id="message"
|
onclick={open_onclick}
|
||||||
class="w-100 form-control"
|
|
||||||
/>
|
/>
|
||||||
|
if !logged_in.data.unwrap_or(true) {
|
||||||
|
<input
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary mt-2"
|
||||||
|
value="Login"
|
||||||
|
onclick={login_onclick}
|
||||||
|
/>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
|
||||||
match msg {
|
|
||||||
HomeMessage::SubmittedMessage(msg) => {
|
|
||||||
let msg = ChatMessage {
|
|
||||||
message: msg.clone(),
|
|
||||||
time: Local::now(),
|
|
||||||
};
|
|
||||||
self.messages.push(msg.clone());
|
|
||||||
let mut buf = Vec::new();
|
|
||||||
ciborium::into_writer(&msg, &mut buf).unwrap();
|
|
||||||
self.chat_ws.as_ref().unwrap().send_binary(&buf).unwrap();
|
|
||||||
true
|
|
||||||
}
|
|
||||||
HomeMessage::RecievedMessage(msg) => {
|
|
||||||
self.messages.push(msg);
|
|
||||||
true
|
|
||||||
}
|
|
||||||
HomeMessage::WsClose(close_event) => {
|
|
||||||
let code = close_event.code();
|
|
||||||
log!("WS connection closed with code", code);
|
|
||||||
if code == 4000 {
|
|
||||||
log!("Unauthorized, redirecting to login page");
|
|
||||||
ctx.link().navigator().unwrap().push(&Route::Login);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if self.ws_state == WsState::Closed {
|
|
||||||
log!("Already closed");
|
|
||||||
if !self.ws_reconnecting {
|
|
||||||
log!("Reconnecting in 5s");
|
|
||||||
self.ws_reconnecting = true;
|
|
||||||
ctx.link().send_future(async move {
|
|
||||||
sleep(Duration::from_secs(5)).await;
|
|
||||||
HomeMessage::WsReconnect
|
|
||||||
});
|
|
||||||
}
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
log!("Reconnecting");
|
|
||||||
self.connect_ws(ctx);
|
|
||||||
self.ws_state = WsState::Closed;
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
HomeMessage::WsOpen => {
|
|
||||||
if self.ws_state != WsState::Open {
|
|
||||||
log!("WS connection opened");
|
|
||||||
self.messages.clear();
|
|
||||||
self.ws_state = WsState::Open;
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
HomeMessage::WsReconnect => {
|
|
||||||
if self.ws_state != WsState::Open {
|
|
||||||
log!("Reconnecting");
|
|
||||||
self.ws_reconnecting = false;
|
|
||||||
self.connect_ws(ctx);
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
|
|
||||||
if let Some(message_container) = self.message_container_ref.cast::<web_sys::Element>() {
|
|
||||||
message_container.set_scroll_top(message_container.scroll_height());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,41 @@
|
|||||||
use crate::{components::Nav, Route};
|
use crate::components::Nav;
|
||||||
use gloo::net::http::Request;
|
use common::LoggedInResponse;
|
||||||
|
use gloo::{
|
||||||
|
history::{BrowserHistory, History},
|
||||||
|
net::http::Request,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use yew::{platform::spawn_local, prelude::*};
|
use yew::{platform::spawn_local, prelude::*};
|
||||||
use yew_router::hooks::use_navigator;
|
use yew_hooks::use_mount;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct LoginQuery {
|
||||||
|
pub redirect: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LoginQuery {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
redirect: "/".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
#[allow(non_snake_case)] // Component names should be in PascalCase
|
#[allow(non_snake_case)] // Component names should be in PascalCase
|
||||||
pub fn Login() -> Html {
|
pub fn Login() -> Html {
|
||||||
let disable_input = use_state(|| false);
|
let disable_input = use_state(|| false);
|
||||||
let navigator = use_navigator();
|
let query_params = BrowserHistory::new()
|
||||||
|
.location()
|
||||||
|
.query::<LoginQuery>()
|
||||||
|
.unwrap_or_default();
|
||||||
|
let redirect = query_params.redirect;
|
||||||
let login_submitted = {
|
let login_submitted = {
|
||||||
let disable_input = disable_input.clone();
|
let disable_input = disable_input.clone();
|
||||||
let navigator = navigator.clone();
|
let redirect = redirect.clone();
|
||||||
move || {
|
move || {
|
||||||
disable_input.set(true);
|
disable_input.set(true);
|
||||||
let navigator = navigator.clone();
|
let redirect = redirect.clone();
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
let location = web_sys::window().unwrap().location();
|
let location = web_sys::window().unwrap().location();
|
||||||
let login_url = format!(
|
let login_url = format!(
|
||||||
@ -27,7 +49,7 @@ pub fn Login() -> Html {
|
|||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
navigator.as_ref().unwrap().push(&Route::Home);
|
BrowserHistory::new().push(&redirect);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -39,10 +61,36 @@ pub fn Login() -> Html {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
{
|
||||||
|
let redirect = redirect.clone();
|
||||||
|
use_mount(move || {
|
||||||
|
spawn_local(async move {
|
||||||
|
let location = web_sys::window().unwrap().location();
|
||||||
|
let logged_in_url = format!(
|
||||||
|
"{}//{}/api/logged_in",
|
||||||
|
location.protocol().unwrap(),
|
||||||
|
location.host().unwrap()
|
||||||
|
);
|
||||||
|
let resp = Request::get(&logged_in_url)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.json::<LoggedInResponse>()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
if resp.logged_in {
|
||||||
|
BrowserHistory::new().push(&redirect);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
html! {
|
html! {
|
||||||
<>
|
<>
|
||||||
<Nav />
|
<Nav />
|
||||||
<div class="container-fluid d-flex flex-column align-items-center" style="max-width: 570px">
|
<div
|
||||||
|
class="container-fluid d-flex flex-column align-items-center"
|
||||||
|
style="max-width: 570px"
|
||||||
|
>
|
||||||
<h1>{ "Login" }</h1>
|
<h1>{ "Login" }</h1>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
use axum::async_trait;
|
|
||||||
use axum::body::Body;
|
use axum::body::Body;
|
||||||
use axum::extract::ws::{self, CloseFrame, Message, WebSocket};
|
use axum::extract::ws::{self, CloseFrame, Message, WebSocket};
|
||||||
use axum::extract::{Request, State, WebSocketUpgrade};
|
use axum::extract::{Request, State, WebSocketUpgrade};
|
||||||
@ -6,10 +5,11 @@ use axum::http::header::CONTENT_TYPE;
|
|||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
use axum::routing::post;
|
use axum::routing::post;
|
||||||
|
use axum::{async_trait, Json};
|
||||||
use axum::{routing::get, Router};
|
use axum::{routing::get, Router};
|
||||||
use axum_login::{AuthManagerLayerBuilder, AuthSession, AuthUser, AuthnBackend, UserId};
|
use axum_login::{AuthManagerLayerBuilder, AuthSession, AuthUser, AuthnBackend, UserId};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use common::ChatMessage;
|
use common::{ChatMessage, LoggedInResponse};
|
||||||
use futures::stream::SplitSink;
|
use futures::stream::SplitSink;
|
||||||
use futures::{SinkExt, StreamExt};
|
use futures::{SinkExt, StreamExt};
|
||||||
use slab::Slab;
|
use slab::Slab;
|
||||||
@ -131,6 +131,20 @@ async fn login(mut auth_session: AuthSession<DummyAuthBackend>) -> Response {
|
|||||||
StatusCode::OK.into_response()
|
StatusCode::OK.into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn logout(mut auth_session: AuthSession<DummyAuthBackend>) -> Response {
|
||||||
|
if auth_session.logout().await.is_err() {
|
||||||
|
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
|
||||||
|
}
|
||||||
|
StatusCode::OK.into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn logged_in(auth_session: AuthSession<DummyAuthBackend>) -> Response {
|
||||||
|
let resp = LoggedInResponse {
|
||||||
|
logged_in: auth_session.user.is_some(),
|
||||||
|
};
|
||||||
|
Json(resp).into_response()
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let opt = Opt::parse();
|
let opt = Opt::parse();
|
||||||
@ -157,7 +171,9 @@ async fn main() {
|
|||||||
let api = Router::new()
|
let api = Router::new()
|
||||||
//.route_layer(login_required!(DummyAuthBackend))
|
//.route_layer(login_required!(DummyAuthBackend))
|
||||||
.route("/chat_ws", get(chat_ws))
|
.route("/chat_ws", get(chat_ws))
|
||||||
.route("/login", post(login));
|
.route("/login", post(login))
|
||||||
|
.route("/logout", post(logout))
|
||||||
|
.route("/logged_in", get(logged_in));
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.nest("/api", api)
|
.nest("/api", api)
|
||||||
|
Loading…
Reference in New Issue
Block a user