Refactor auth API client. Implement proper error handling.
This commit is contained in:
parent
da3874986f
commit
4c0d5ea2d8
|
@ -5,20 +5,45 @@ use wasm_bindgen::JsCast;
|
||||||
use wasm_bindgen_futures::JsFuture;
|
use wasm_bindgen_futures::JsFuture;
|
||||||
use web_sys::{console, Request, RequestCredentials, RequestInit, RequestMode, Response};
|
use web_sys::{console, Request, RequestCredentials, RequestInit, RequestMode, Response};
|
||||||
|
|
||||||
|
/// A struct representing an error coming back from the REST API
|
||||||
|
/// endpoint. The API server encodes any errors as JSON objects with a
|
||||||
|
/// "message" property containing the error, and a bad status code.
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct ApiError {
|
||||||
|
message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct LoginResponse {
|
struct LoginResponse {
|
||||||
jwt_token: String,
|
jwt_token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn make_request(request: Request) -> Result<JsValue, UiError> {
|
async fn make_request<T>(request: Request) -> Result<T, UiError>
|
||||||
|
where
|
||||||
|
T: for<'a> Deserialize<'a>,
|
||||||
|
{
|
||||||
let window = web_sys::window().unwrap();
|
let window = web_sys::window().unwrap();
|
||||||
|
|
||||||
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
|
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
|
||||||
let resp: Response = resp_value.dyn_into().unwrap();
|
let resp: Response = resp_value.dyn_into().unwrap();
|
||||||
|
let ok = resp.ok();
|
||||||
|
|
||||||
let json = JsFuture::from(resp.json()?).await?;
|
let json = JsFuture::from(resp.json()?).await?;
|
||||||
console::log_1(&json);
|
console::log_1(&json);
|
||||||
Ok(json)
|
//if ok, attempt to deserialize into T.
|
||||||
|
//if not ok, attempt to deserialize into struct with message, and fall back
|
||||||
|
//if that fails.
|
||||||
|
if ok {
|
||||||
|
let data: T = json.into_serde()?;
|
||||||
|
Ok(data)
|
||||||
|
} else {
|
||||||
|
let data: ApiError = json.into_serde()?;
|
||||||
|
Err(UiError::ApiError(data.message.unwrap_or_else(|| {
|
||||||
|
let status_text = resp.status_text();
|
||||||
|
let status = resp.status();
|
||||||
|
format!("[{}] - {} - unknown api error", status, status_text)
|
||||||
|
})))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn login(username: &str, password: &str) -> Result<String, UiError> {
|
pub async fn login(username: &str, password: &str) -> Result<String, UiError> {
|
||||||
|
@ -42,10 +67,7 @@ pub async fn login(username: &str, password: &str) -> Result<String, UiError> {
|
||||||
request.headers().set("Content-Type", "application/json")?;
|
request.headers().set("Content-Type", "application/json")?;
|
||||||
request.headers().set("Accept", "application/json")?;
|
request.headers().set("Accept", "application/json")?;
|
||||||
|
|
||||||
//TODO don't unwrap the response. OR... change it so we have a standard response.
|
let response: LoginResponse = make_request(request).await?;
|
||||||
let response = make_request(request).await?;
|
|
||||||
let response: LoginResponse = response.into_serde().unwrap();
|
|
||||||
|
|
||||||
Ok(response.jwt_token)
|
Ok(response.jwt_token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,8 +84,6 @@ pub async fn refresh_jwt() -> Result<String, UiError> {
|
||||||
request.headers().set("Accept", "application/json")?;
|
request.headers().set("Accept", "application/json")?;
|
||||||
|
|
||||||
//TODO don't unwrap the response. OR... change it so we have a standard response.
|
//TODO don't unwrap the response. OR... change it so we have a standard response.
|
||||||
let response = make_request(request).await?;
|
let response: LoginResponse = make_request(request).await?;
|
||||||
let response: LoginResponse = response.into_serde().unwrap();
|
|
||||||
|
|
||||||
Ok(response.jwt_token)
|
Ok(response.jwt_token)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
use crate::api;
|
||||||
|
use crate::error::UiError;
|
||||||
|
use crate::state::{Action, WebUiDispatcher};
|
||||||
|
use std::rc::Rc;
|
||||||
|
use wasm_bindgen::{prelude::Closure, JsCast};
|
||||||
|
use wasm_bindgen_futures::spawn_local;
|
||||||
|
use web_sys::FocusEvent;
|
||||||
|
use yew::prelude::*;
|
||||||
|
use yewdux::dispatch::Dispatcher;
|
||||||
|
use yewdux::prelude::*;
|
||||||
|
use yewtil::NeqAssign;
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub(crate) struct YewduxErrorMessage {
|
||||||
|
dispatch: WebUiDispatcher,
|
||||||
|
link: ComponentLink<YewduxErrorMessage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) type ErrorMessage = WithDispatch<YewduxErrorMessage>;
|
||||||
|
|
||||||
|
impl YewduxErrorMessage {
|
||||||
|
fn view_error(&self, error: &str) -> Html {
|
||||||
|
html! {
|
||||||
|
<div class="alert alert-danger" role="alert">
|
||||||
|
{ error }
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for YewduxErrorMessage {
|
||||||
|
type Message = ();
|
||||||
|
type Properties = WebUiDispatcher;
|
||||||
|
|
||||||
|
fn create(dispatch: Self::Properties, link: ComponentLink<Self>) -> Self {
|
||||||
|
Self { dispatch, link }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, action: Self::Message) -> ShouldRender {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn change(&mut self, dispatch: Self::Properties) -> ShouldRender {
|
||||||
|
self.dispatch.neq_assign(dispatch)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rendered(&mut self, _first_render: bool) {}
|
||||||
|
|
||||||
|
fn view(&self) -> Html {
|
||||||
|
html! {
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
for self.dispatch.state().error_messages.iter().map(|error| {
|
||||||
|
self.view_error(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn destroy(&mut self) {}
|
||||||
|
}
|
|
@ -1 +1,2 @@
|
||||||
|
pub mod error_message;
|
||||||
pub mod login;
|
pub mod login;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::components::error_message::ErrorMessage;
|
||||||
use crate::components::login::Login;
|
use crate::components::login::Login;
|
||||||
use rooms::RoomList;
|
use rooms::RoomList;
|
||||||
use rooms::YewduxRoomList;
|
use rooms::YewduxRoomList;
|
||||||
|
@ -42,6 +43,7 @@ fn render_route(routes: &AppRoute) -> Html {
|
||||||
html! {
|
html! {
|
||||||
<div>
|
<div>
|
||||||
<Login />
|
<Login />
|
||||||
|
<ErrorMessage />
|
||||||
<RoomList />
|
<RoomList />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -49,47 +51,6 @@ fn render_route(routes: &AppRoute) -> Html {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// struct AppMenu;
|
|
||||||
|
|
||||||
// impl Component for AppMenu {
|
|
||||||
// type Message = ();
|
|
||||||
// type Properties = ();
|
|
||||||
|
|
||||||
// fn create(_: Self::Properties, _link: ComponentLink<Self>) -> Self {
|
|
||||||
// Self
|
|
||||||
// }
|
|
||||||
|
|
||||||
// fn update(&mut self, _msg: Self::Message) -> ShouldRender {
|
|
||||||
// false
|
|
||||||
// }
|
|
||||||
|
|
||||||
// fn change(&mut self, _: Self::Properties) -> ShouldRender {
|
|
||||||
// false
|
|
||||||
// }
|
|
||||||
|
|
||||||
// fn view(&self) -> Html {
|
|
||||||
// html! {
|
|
||||||
// <ul>
|
|
||||||
// <li>
|
|
||||||
// <AppAnchor route=AppRoute::Index>{"Home"}</AppAnchor>
|
|
||||||
// </li>
|
|
||||||
// <li>
|
|
||||||
// <AppAnchor route=AppRoute::Oaths>{"Oaths"}</AppAnchor>
|
|
||||||
// </li>
|
|
||||||
// <li>
|
|
||||||
// <AppAnchor route=AppRoute::Commitments>{"Commitments"}</AppAnchor>
|
|
||||||
// </li>
|
|
||||||
// <li>
|
|
||||||
// <AppAnchor route=AppRoute::Studies>{"Studies"}</AppAnchor>
|
|
||||||
// </li>
|
|
||||||
// <li>
|
|
||||||
// <AppAnchor route=AppRoute::RunicDivination>{"Runic Divination"}</AppAnchor>
|
|
||||||
// </li>
|
|
||||||
// </ul>
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
struct App;
|
struct App;
|
||||||
|
|
||||||
impl Component for App {
|
impl Component for App {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::api;
|
use crate::api;
|
||||||
use crate::error::UiError;
|
use crate::error::UiError;
|
||||||
use crate::state::{Action, Room, WebUiDispatcher};
|
use crate::state::{Action, DispatchExt, Room, WebUiDispatcher};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use wasm_bindgen_futures::spawn_local;
|
use wasm_bindgen_futures::spawn_local;
|
||||||
use web_sys::console;
|
use web_sys::console;
|
||||||
|
@ -44,10 +44,13 @@ async fn load_rooms(dispatch: &WebUiDispatcher) -> Result<(), UiError> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn do_refresh_jwt(dispatch: &WebUiDispatcher) -> Result<(), UiError> {
|
async fn do_refresh_jwt(dispatch: &WebUiDispatcher) {
|
||||||
let jwt = api::auth::refresh_jwt().await?;
|
let refresh = api::auth::refresh_jwt().await;
|
||||||
dispatch.send(Action::UpdateJwt(jwt));
|
|
||||||
Ok(())
|
match refresh {
|
||||||
|
Ok(jwt) => dispatch.send(Action::UpdateJwt(jwt)),
|
||||||
|
Err(e) => dispatch.dispatch_error(e),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component for YewduxRoomList {
|
impl Component for YewduxRoomList {
|
||||||
|
@ -101,9 +104,7 @@ impl Component for YewduxRoomList {
|
||||||
|
|
||||||
let refresh_jwt = self.link.callback(move |_| {
|
let refresh_jwt = self.link.callback(move |_| {
|
||||||
let dispatch = dispatch3.clone();
|
let dispatch = dispatch3.clone();
|
||||||
spawn_local(async move {
|
spawn_local(async move { do_refresh_jwt(&*dispatch).await });
|
||||||
do_refresh_jwt(&*dispatch).await;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use crate::error::UiError;
|
||||||
|
use wasm_bindgen::{prelude::Closure, JsCast};
|
||||||
use yewdux::prelude::*;
|
use yewdux::prelude::*;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -10,27 +12,33 @@ pub(crate) struct Room {
|
||||||
pub(crate) struct WebUiState {
|
pub(crate) struct WebUiState {
|
||||||
pub jwt_token: Option<String>,
|
pub jwt_token: Option<String>,
|
||||||
pub rooms: Vec<Room>,
|
pub rooms: Vec<Room>,
|
||||||
|
pub error_messages: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) enum Action {
|
pub(crate) enum Action {
|
||||||
UpdateJwt(String),
|
UpdateJwt(String),
|
||||||
AddRoom(Room),
|
AddRoom(Room),
|
||||||
|
ErrorMessage(UiError),
|
||||||
|
ClearErrorMessage,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Reducer for WebUiState {
|
impl Reducer for WebUiState {
|
||||||
type Action = Action;
|
type Action = Action;
|
||||||
|
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self {
|
Self::default()
|
||||||
jwt_token: None,
|
|
||||||
rooms: vec![],
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reduce(&mut self, action: Self::Action) -> bool {
|
fn reduce(&mut self, action: Self::Action) -> bool {
|
||||||
match action {
|
match action {
|
||||||
Action::UpdateJwt(jwt_token) => self.jwt_token = Some(jwt_token),
|
Action::UpdateJwt(jwt_token) => self.jwt_token = Some(jwt_token),
|
||||||
Action::AddRoom(room) => self.rooms.push(room.clone()),
|
Action::AddRoom(room) => self.rooms.push(room.clone()),
|
||||||
|
Action::ErrorMessage(error) => self.error_messages.push(error.to_string()),
|
||||||
|
Action::ClearErrorMessage => {
|
||||||
|
if self.error_messages.len() > 0 {
|
||||||
|
self.error_messages.remove(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
true
|
true
|
||||||
|
@ -38,3 +46,33 @@ impl Reducer for WebUiState {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) type WebUiDispatcher = DispatchProps<ReducerStore<WebUiState>>;
|
pub(crate) type WebUiDispatcher = DispatchProps<ReducerStore<WebUiState>>;
|
||||||
|
|
||||||
|
pub(crate) trait DispatchExt {
|
||||||
|
/// Dispatch an error message and then clear it from the state
|
||||||
|
/// after a few seconds.
|
||||||
|
fn dispatch_error(&self, error: UiError);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DispatchExt for WebUiDispatcher {
|
||||||
|
fn dispatch_error(&self, error: UiError) {
|
||||||
|
self.send(Action::ErrorMessage(error));
|
||||||
|
|
||||||
|
// This is a very hacky way to do this. At the very least, we
|
||||||
|
// should not leak memory, and preferably there's a cleaner
|
||||||
|
// way to actually dispatch the clear action.
|
||||||
|
let window = web_sys::window().unwrap();
|
||||||
|
let dispatch = self.clone();
|
||||||
|
let clear_it = Closure::wrap(
|
||||||
|
Box::new(move || dispatch.send(Action::ClearErrorMessage)) as Box<dyn Fn()>
|
||||||
|
);
|
||||||
|
|
||||||
|
window
|
||||||
|
.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||||
|
clear_it.as_ref().unchecked_ref(),
|
||||||
|
4000,
|
||||||
|
)
|
||||||
|
.expect("could not add clear error handler.");
|
||||||
|
|
||||||
|
clear_it.forget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue