Refactor auth API client. Implement proper error handling.

This commit is contained in:
projectmoon 2021-06-12 21:45:10 +00:00
parent da3874986f
commit 4c0d5ea2d8
6 changed files with 145 additions and 62 deletions

View File

@ -5,20 +5,45 @@ use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
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)]
struct LoginResponse {
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 resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
let resp: Response = resp_value.dyn_into().unwrap();
let ok = resp.ok();
let json = JsFuture::from(resp.json()?).await?;
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> {
@ -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("Accept", "application/json")?;
//TODO don't unwrap the response. OR... change it so we have a standard response.
let response = make_request(request).await?;
let response: LoginResponse = response.into_serde().unwrap();
let response: LoginResponse = make_request(request).await?;
Ok(response.jwt_token)
}
@ -62,8 +84,6 @@ pub async fn refresh_jwt() -> Result<String, UiError> {
request.headers().set("Accept", "application/json")?;
//TODO don't unwrap the response. OR... change it so we have a standard response.
let response = make_request(request).await?;
let response: LoginResponse = response.into_serde().unwrap();
let response: LoginResponse = make_request(request).await?;
Ok(response.jwt_token)
}

View File

@ -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) {}
}

View File

@ -1 +1,2 @@
pub mod error_message;
pub mod login;

View File

@ -1,3 +1,4 @@
use crate::components::error_message::ErrorMessage;
use crate::components::login::Login;
use rooms::RoomList;
use rooms::YewduxRoomList;
@ -42,6 +43,7 @@ fn render_route(routes: &AppRoute) -> Html {
html! {
<div>
<Login />
<ErrorMessage />
<RoomList />
</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;
impl Component for App {

View File

@ -1,6 +1,6 @@
use crate::api;
use crate::error::UiError;
use crate::state::{Action, Room, WebUiDispatcher};
use crate::state::{Action, DispatchExt, Room, WebUiDispatcher};
use std::sync::Arc;
use wasm_bindgen_futures::spawn_local;
use web_sys::console;
@ -44,10 +44,13 @@ async fn load_rooms(dispatch: &WebUiDispatcher) -> Result<(), UiError> {
Ok(())
}
async fn do_refresh_jwt(dispatch: &WebUiDispatcher) -> Result<(), UiError> {
let jwt = api::auth::refresh_jwt().await?;
dispatch.send(Action::UpdateJwt(jwt));
Ok(())
async fn do_refresh_jwt(dispatch: &WebUiDispatcher) {
let refresh = api::auth::refresh_jwt().await;
match refresh {
Ok(jwt) => dispatch.send(Action::UpdateJwt(jwt)),
Err(e) => dispatch.dispatch_error(e),
}
}
impl Component for YewduxRoomList {
@ -101,9 +104,7 @@ impl Component for YewduxRoomList {
let refresh_jwt = self.link.callback(move |_| {
let dispatch = dispatch3.clone();
spawn_local(async move {
do_refresh_jwt(&*dispatch).await;
});
spawn_local(async move { do_refresh_jwt(&*dispatch).await });
});
html! {

View File

@ -1,3 +1,5 @@
use crate::error::UiError;
use wasm_bindgen::{prelude::Closure, JsCast};
use yewdux::prelude::*;
#[derive(Clone)]
@ -10,27 +12,33 @@ pub(crate) struct Room {
pub(crate) struct WebUiState {
pub jwt_token: Option<String>,
pub rooms: Vec<Room>,
pub error_messages: Vec<String>,
}
pub(crate) enum Action {
UpdateJwt(String),
AddRoom(Room),
ErrorMessage(UiError),
ClearErrorMessage,
}
impl Reducer for WebUiState {
type Action = Action;
fn new() -> Self {
Self {
jwt_token: None,
rooms: vec![],
}
Self::default()
}
fn reduce(&mut self, action: Self::Action) -> bool {
match action {
Action::UpdateJwt(jwt_token) => self.jwt_token = Some(jwt_token),
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
@ -38,3 +46,33 @@ impl Reducer for 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();
}
}