Web API, Web UI #86

Merged
projectmoon merged 37 commits from web-api into master 2021-07-15 15:04:54 +00:00
2 changed files with 58 additions and 28 deletions
Showing only changes of commit 08070ed8ad - Show all commits

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()),