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.
This commit is contained in:
projectmoon 2021-06-22 14:33:55 +00:00
parent b9381eecf4
commit 08070ed8ad
2 changed files with 58 additions and 28 deletions

View File

@ -1,58 +1,63 @@
use crate::{ use crate::{
api, api,
error::UiError, state::{Action, Claims, Room, WebUiDispatcher},
state::{Action, Room, WebUiDispatcher},
}; };
use jsonwebtoken::{ use jsonwebtoken::{
dangerous_insecure_decode_with_validation as decode_without_verify, Validation, dangerous_insecure_decode_with_validation as decode_without_verify, Validation,
}; };
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)] pub(crate) type LogicResult = Result<Vec<Action>, Action>;
struct Claims {
exp: usize, trait LogicResultExt {
sub: String, /// 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<Action>;
}
impl LogicResultExt for LogicResult {
fn actions(self) -> Vec<Action> {
self.unwrap_or_else(|err_action| vec![err_action])
}
} }
fn map_to_vec(action: Option<Action>) -> Vec<Action> { fn map_to_vec(action: Option<Action>) -> Vec<Action> {
action.map(|a| vec![a]).unwrap_or_default() action.map(|a| vec![a]).unwrap_or_default()
} }
async fn ensure_jwt(dispatch: &WebUiDispatcher) -> Result<(String, Option<Action>), UiError> { async fn refresh_ensured_jwt() -> Result<(String, Option<Action>), Action> {
//TODO we should add a logout action and return it from here if there's an error when refreshing. api::auth::refresh_jwt()
//TODO somehow have to handle actions on an error! .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>), Action> {
//TODO lots of clones here. can we avoid? //TODO lots of clones here. can we avoid?
use jsonwebtoken::errors::ErrorKind; use jsonwebtoken::errors::ErrorKind;
let token = dispatch.state().jwt_token.as_deref().unwrap_or_default(); let token = dispatch.state().jwt_token.as_deref().unwrap_or_default();
let validation = let validation: Result<Claims, _> =
decode_without_verify::<Claims>(token, &Validation::default()).map(|data| data.claims); decode_without_verify(token, &Validation::default()).map(|data| data.claims);
//If valid, simply return token. If expired, attempt to refresh. //If valid, simply return token. If expired, attempt to refresh.
//Otherwise, bubble error. //Otherwise, bubble error.
let token_and_action = match validation { let token_and_action = match validation {
Ok(_) => (token.to_owned(), None), Ok(_) => (token.to_owned(), None),
Err(e) if matches!(e.kind(), ErrorKind::ExpiredSignature) => { Err(e) if matches!(e.kind(), ErrorKind::ExpiredSignature) => refresh_ensured_jwt().await?,
match api::auth::refresh_jwt().await { Err(_) => return Err(Action::Logout), //TODO carry error inside Logout?
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()),
}; };
Ok(token_and_action) Ok(token_and_action)
} }
pub(crate) async fn fetch_rooms(dispatch: &WebUiDispatcher) -> Result<Vec<Action>, UiError> { pub(crate) async fn fetch_rooms(dispatch: &WebUiDispatcher) -> LogicResult {
let (jwt, jwt_update) = ensure_jwt(dispatch) let (jwt, jwt_update) = ensure_jwt(dispatch)
.await .await
.map(|(token, update)| (token, map_to_vec(update)))?; .map(|(token, update)| (token, map_to_vec(update)))?;
//Use new JWT to list rooms from graphql. let rooms: Vec<Action> = api::dicebot::rooms_for_user(&jwt, &dispatch.state().username)
//TODO get username from state. .await
let rooms: Vec<Action> = api::dicebot::rooms_for_user(&jwt, "@projectmoon:agnos.is") .map_err(|e| Action::ErrorMessage(e))?
.await?
.into_iter() .into_iter()
.map(|room| { .map(|room| {
Action::AddRoom(Room { Action::AddRoom(Room {

View File

@ -1,7 +1,17 @@
use crate::error::UiError; 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 wasm_bindgen::{prelude::Closure, JsCast};
use yewdux::prelude::*; use yewdux::prelude::*;
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct Claims {
exp: usize,
sub: String,
}
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct Room { pub(crate) struct Room {
pub room_id: String, pub room_id: String,
@ -27,6 +37,7 @@ 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 error_messages: Vec<String>,
pub username: String,
} }
pub(crate) enum Action { pub(crate) enum Action {
@ -36,8 +47,24 @@ pub(crate) enum Action {
ClearErrorMessage, ClearErrorMessage,
ChangeAuthState(AuthState), ChangeAuthState(AuthState),
Login(String), Login(String),
Logout,
} }
impl WebUiState {
fn login(&mut self, jwt_token: String) {
let validation: Result<Claims, _> =
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 { impl Reducer for WebUiState {
type Action = Action; type Action = Action;
@ -48,10 +75,8 @@ impl Reducer for WebUiState {
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::Login(jwt_token) => { Action::Login(jwt_token) => self.login(jwt_token),
self.jwt_token = Some(jwt_token); Action::Logout => (),
self.auth_state = AuthState::LoggedIn;
}
Action::AddRoom(room) => self.rooms.push(room.clone()), Action::AddRoom(room) => self.rooms.push(room.clone()),
Action::ChangeAuthState(auth_state) => self.auth_state = auth_state, Action::ChangeAuthState(auth_state) => self.auth_state = auth_state,
Action::ErrorMessage(error) => self.error_messages.push(error.to_string()), Action::ErrorMessage(error) => self.error_messages.push(error.to_string()),