Web API, Web UI #86
|
@ -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 {
|
||||||
|
|
|
@ -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()),
|
||||||
|
|
Loading…
Reference in New Issue