From b9381eecf44db7e9fb7486cbabf9c8e0465ff9d1 Mon Sep 17 00:00:00 2001 From: projectmoon Date: Sun, 20 Jun 2021 21:45:55 +0000 Subject: [PATCH] Begin creating logic layer in web UI. --- Cargo.lock | 1 + web-ui/crate/Cargo.toml | 1 + web-ui/crate/src/api/auth.rs | 1 - web-ui/crate/src/error.rs | 3 ++ web-ui/crate/src/lib.rs | 1 + web-ui/crate/src/logic.rs | 66 ++++++++++++++++++++++++++++++++++++ 6 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 web-ui/crate/src/logic.rs diff --git a/Cargo.lock b/Cargo.lock index 9a30a1f..f35ea9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3578,6 +3578,7 @@ dependencies = [ "graphql_client", "graphql_client_web", "js-sys", + "jsonwebtoken", "serde", "serde_json", "tenebrous-api", diff --git a/web-ui/crate/Cargo.toml b/web-ui/crate/Cargo.toml index f9434e5..c65f87d 100644 --- a/web-ui/crate/Cargo.toml +++ b/web-ui/crate/Cargo.toml @@ -25,6 +25,7 @@ graphql_client_web = { git = "https://github.com/graphql-rust/graphql-client", b serde = { version = "1.0.67", features = ["derive"] } serde_json = {version = "1.0" } thiserror = "1.0" +jsonwebtoken = "7.2" [dependencies.web-sys] version = "0.3" diff --git a/web-ui/crate/src/api/auth.rs b/web-ui/crate/src/api/auth.rs index 518459a..9751416 100644 --- a/web-ui/crate/src/api/auth.rs +++ b/web-ui/crate/src/api/auth.rs @@ -83,7 +83,6 @@ pub async fn refresh_jwt() -> Result { 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: LoginResponse = make_request(request).await?; Ok(response.jwt_token) } diff --git a/web-ui/crate/src/error.rs b/web-ui/crate/src/error.rs index a4f9d16..dfc2329 100644 --- a/web-ui/crate/src/error.rs +++ b/web-ui/crate/src/error.rs @@ -18,6 +18,9 @@ pub enum UiError { #[error("(de)serialization error: {0}")] SerdeError(#[from] serde_json::Error), + + #[error("JWT validation error: {0}")] + JwtError(#[from] jsonwebtoken::errors::Error), } impl From for UiError { diff --git a/web-ui/crate/src/lib.rs b/web-ui/crate/src/lib.rs index e760859..ed68e69 100644 --- a/web-ui/crate/src/lib.rs +++ b/web-ui/crate/src/lib.rs @@ -16,6 +16,7 @@ pub mod api; pub mod components; pub mod error; pub mod grpc; +pub mod logic; pub mod rooms; pub mod state; diff --git a/web-ui/crate/src/logic.rs b/web-ui/crate/src/logic.rs new file mode 100644 index 0000000..4de68c2 --- /dev/null +++ b/web-ui/crate/src/logic.rs @@ -0,0 +1,66 @@ +use crate::{ + api, + error::UiError, + state::{Action, 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, +} + +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! + + //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); + + //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()), + }; + + Ok(token_and_action) +} + +pub(crate) async fn fetch_rooms(dispatch: &WebUiDispatcher) -> Result, UiError> { + 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? + .into_iter() + .map(|room| { + Action::AddRoom(Room { + room_id: room.room_id, + display_name: room.display_name, + }) + }) + .collect(); + + Ok(rooms.into_iter().chain(jwt_update).collect()) +}