From 08070ed8ad7101275045b2778b7a761c4a4ae19f Mon Sep 17 00:00:00 2001 From: projectmoon Date: Tue, 22 Jun 2021 14:33:55 +0000 Subject: [PATCH] Wire up stateful logic handling, to be called from components. Add username to state when logging in. Ensure and refresh JWTs before making API calls if necessary. --- web-ui/crate/src/logic.rs | 53 +++++++++++++++++++++------------------ web-ui/crate/src/state.rs | 33 +++++++++++++++++++++--- 2 files changed, 58 insertions(+), 28 deletions(-) diff --git a/web-ui/crate/src/logic.rs b/web-ui/crate/src/logic.rs index 4de68c2..a71f142 100644 --- a/web-ui/crate/src/logic.rs +++ b/web-ui/crate/src/logic.rs @@ -1,58 +1,63 @@ use crate::{ api, - error::UiError, - state::{Action, Room, WebUiDispatcher}, + state::{Action, Claims, Room, WebUiDispatcher}, }; use jsonwebtoken::{ dangerous_insecure_decode_with_validation as decode_without_verify, Validation, }; -use serde::{Deserialize, Serialize}; -#[derive(Debug, Serialize, Deserialize)] -struct Claims { - exp: usize, - sub: String, +pub(crate) type LogicResult = Result, Action>; + +trait LogicResultExt { + /// Consumes self into the vec of Actions to apply to state, + /// either the list of successful actions, or a list containing + /// the error action. + fn actions(self) -> Vec; +} + +impl LogicResultExt for LogicResult { + fn actions(self) -> Vec { + self.unwrap_or_else(|err_action| vec![err_action]) + } } fn map_to_vec(action: Option) -> Vec { action.map(|a| vec![a]).unwrap_or_default() } -async fn ensure_jwt(dispatch: &WebUiDispatcher) -> Result<(String, Option), UiError> { - //TODO we should add a logout action and return it from here if there's an error when refreshing. - //TODO somehow have to handle actions on an error! +async fn refresh_ensured_jwt() -> Result<(String, Option), Action> { + api::auth::refresh_jwt() + .await + .map(|new_jwt| (new_jwt.clone(), Some(Action::UpdateJwt(new_jwt)))) + .map_err(|_| Action::Logout) +} +async fn ensure_jwt(dispatch: &WebUiDispatcher) -> Result<(String, Option), Action> { //TODO lots of clones here. can we avoid? use jsonwebtoken::errors::ErrorKind; let token = dispatch.state().jwt_token.as_deref().unwrap_or_default(); - let validation = - decode_without_verify::(token, &Validation::default()).map(|data| data.claims); + let validation: Result = + decode_without_verify(token, &Validation::default()).map(|data| data.claims); //If valid, simply return token. If expired, attempt to refresh. //Otherwise, bubble error. let token_and_action = match validation { Ok(_) => (token.to_owned(), None), - Err(e) if matches!(e.kind(), ErrorKind::ExpiredSignature) => { - match api::auth::refresh_jwt().await { - Ok(new_jwt) => (new_jwt.clone(), Some(Action::UpdateJwt(new_jwt))), - Err(e) => return Err(e.into()), //TODO logout action - } - } - Err(e) => return Err(e.into()), + Err(e) if matches!(e.kind(), ErrorKind::ExpiredSignature) => refresh_ensured_jwt().await?, + Err(_) => return Err(Action::Logout), //TODO carry error inside Logout? }; Ok(token_and_action) } -pub(crate) async fn fetch_rooms(dispatch: &WebUiDispatcher) -> Result, UiError> { +pub(crate) async fn fetch_rooms(dispatch: &WebUiDispatcher) -> LogicResult { let (jwt, jwt_update) = ensure_jwt(dispatch) .await .map(|(token, update)| (token, map_to_vec(update)))?; - //Use new JWT to list rooms from graphql. - //TODO get username from state. - let rooms: Vec = api::dicebot::rooms_for_user(&jwt, "@projectmoon:agnos.is") - .await? + let rooms: Vec = api::dicebot::rooms_for_user(&jwt, &dispatch.state().username) + .await + .map_err(|e| Action::ErrorMessage(e))? .into_iter() .map(|room| { Action::AddRoom(Room { diff --git a/web-ui/crate/src/state.rs b/web-ui/crate/src/state.rs index 7d9d56f..875d70f 100644 --- a/web-ui/crate/src/state.rs +++ b/web-ui/crate/src/state.rs @@ -1,7 +1,17 @@ use crate::error::UiError; +use jsonwebtoken::{ + dangerous_insecure_decode_with_validation as decode_without_verify, Validation, +}; +use serde::{Deserialize, Serialize}; use wasm_bindgen::{prelude::Closure, JsCast}; use yewdux::prelude::*; +#[derive(Debug, Serialize, Deserialize)] +pub(crate) struct Claims { + exp: usize, + sub: String, +} + #[derive(Clone)] pub(crate) struct Room { pub room_id: String, @@ -27,6 +37,7 @@ pub(crate) struct WebUiState { pub jwt_token: Option, pub rooms: Vec, pub error_messages: Vec, + pub username: String, } pub(crate) enum Action { @@ -36,8 +47,24 @@ pub(crate) enum Action { ClearErrorMessage, ChangeAuthState(AuthState), Login(String), + Logout, } +impl WebUiState { + fn login(&mut self, jwt_token: String) { + let validation: Result = + decode_without_verify(&jwt_token, &Validation::default()).map(|data| data.claims); + + match validation { + Ok(claims) => { + self.jwt_token = Some(jwt_token); + self.auth_state = AuthState::LoggedIn; + self.username = claims.sub; + } + Err(e) => self.error_messages.push(e.to_string()), + } + } +} impl Reducer for WebUiState { type Action = Action; @@ -48,10 +75,8 @@ impl Reducer for WebUiState { fn reduce(&mut self, action: Self::Action) -> bool { match action { Action::UpdateJwt(jwt_token) => self.jwt_token = Some(jwt_token), - Action::Login(jwt_token) => { - self.jwt_token = Some(jwt_token); - self.auth_state = AuthState::LoggedIn; - } + Action::Login(jwt_token) => self.login(jwt_token), + Action::Logout => (), Action::AddRoom(room) => self.rooms.push(room.clone()), Action::ChangeAuthState(auth_state) => self.auth_state = auth_state, Action::ErrorMessage(error) => self.error_messages.push(error.to_string()),