Web API, Web UI #86

Merged
projectmoon merged 37 commits from web-api into master 2021-07-15 15:04:54 +00:00
7 changed files with 107 additions and 11 deletions
Showing only changes of commit eeacf01cbe - Show all commits

View File

@ -96,15 +96,20 @@ fn create_token<'a>(
Ok(token) Ok(token)
} }
#[derive(Serialize)]
struct LoginResponse {
jwt_token: String,
}
#[rocket::post("/login", data = "<request>")] #[rocket::post("/login", data = "<request>")]
async fn login( async fn login(
request: Json<LoginRequest<'_>>, request: Json<LoginRequest<'_>>,
config: &State<Config>, config: &State<Config>,
cookies: &CookieJar<'_>, cookies: &CookieJar<'_>,
) -> Result<String, Custom<String>> { ) -> Result<Json<LoginResponse>, Custom<String>> {
let token = create_token(request.username, Duration::minutes(1), &config.jwt_secret)?; let token = create_token(request.username, Duration::minutes(1), &config.jwt_secret)?;
let refresh_token = create_token(request.username, Duration::weeks(1), &config.jwt_secret)?; let refresh_token = create_token(request.username, Duration::weeks(1), &config.jwt_secret)?;
cookies.add_private(Cookie::new("refresh_token", refresh_token)); cookies.add_private(Cookie::new("refresh_token", refresh_token));
Ok(token) Ok(Json(LoginResponse { jwt_token: token }))
} }

View File

@ -27,12 +27,22 @@ wasm-bindgen = { version = "0.2" }
wasm-bindgen-futures = "0.4" wasm-bindgen-futures = "0.4"
js-sys = "0.3" js-sys = "0.3"
#grpc-web-client = "0.1" #grpc-web-client = "0.1"
web-sys = "0.3"
graphql_client = { git = "https://github.com/graphql-rust/graphql-client", branch = "master" } graphql_client = { git = "https://github.com/graphql-rust/graphql-client", branch = "master" }
graphql_client_web = { git = "https://github.com/graphql-rust/graphql-client", branch = "master" } graphql_client_web = { git = "https://github.com/graphql-rust/graphql-client", branch = "master" }
serde = { version = "1.0.67", features = ["derive"] } serde = { version = "1.0.67", features = ["derive"] }
thiserror = "1.0" thiserror = "1.0"
[dependencies.web-sys]
version = "0.3"
features = [
'Headers',
'Request',
'RequestInit',
'RequestMode',
'Response',
'Window',
]
# hopefully we can add grpc-web later instead of graphql. # hopefully we can add grpc-web later instead of graphql.
# prost = { version = "0.7.0", default-features = false } # prost = { version = "0.7.0", default-features = false }
# tonic = { git = "https://github.com/hyperium/tonic", branch = "master", default-features = false, features = ["codegen", "prost"] } # tonic = { git = "https://github.com/hyperium/tonic", branch = "master", default-features = false, features = ["codegen", "prost"] }

View File

@ -0,0 +1,46 @@
use crate::error::UiError;
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
use web_sys::{console, Request, RequestInit, RequestMode, Response};
#[derive(Debug, Serialize, Deserialize)]
struct LoginResponse {
jwt_token: String,
}
pub async fn fetch_jwt() -> Result<String, UiError> {
let mut opts = RequestInit::new();
opts.method("POST");
opts.mode(RequestMode::Cors);
opts.body(Some(&JsValue::from_str(
r#"
{ "username": "@projectmoon:agnos.is", "password": "lolol" }
"#,
)));
let url = format!("http://localhost:10000/login");
let request = Request::new_with_str_and_init(&url, &opts)?;
request.headers().set("Content-Type", "application/json")?;
request.headers().set("Accept", "application/json")?;
let window = web_sys::window().unwrap();
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
// `resp_value` is a `Response` object.
assert!(resp_value.is_instance_of::<Response>());
let resp: Response = resp_value.dyn_into().unwrap();
// Convert this other `Promise` into a rust `Future`.
let json = JsFuture::from(resp.json()?).await?;
console::log_1(&json);
// Use serde to parse the JSON into a struct.
let login_response: LoginResponse = json.into_serde().unwrap();
// Send the `Branch` struct back to JS as an `Object`.
Ok(login_response.jwt_token)
}

View File

@ -2,6 +2,9 @@ use graphql_client_web::Response;
use crate::error::UiError; use crate::error::UiError;
pub mod auth;
pub mod dicebot;
/// Extensions to the GraphQL web response type to add convenience, /// Extensions to the GraphQL web response type to add convenience,
/// particularly when working with errors. /// particularly when working with errors.
trait ResponseExt<T> { trait ResponseExt<T> {
@ -32,5 +35,3 @@ impl<T> ResponseExt<T> for Response<T> {
Ok(data) Ok(data)
} }
} }
pub mod dicebot;

View File

@ -9,4 +9,17 @@ pub enum UiError {
/// General API error, collecting errors from graphql server. /// General API error, collecting errors from graphql server.
#[error("error: {0}")] #[error("error: {0}")]
ApiError(String), ApiError(String),
#[error("error: {0}")]
JsError(String),
}
impl From<wasm_bindgen::JsValue> for UiError {
fn from(js_error: wasm_bindgen::JsValue) -> UiError {
UiError::JsError(
js_error
.as_string()
.unwrap_or("unknown JS error".to_string()),
)
}
} }

View File

@ -38,6 +38,12 @@ async fn load_rooms(dispatch: &WebUiDispatcher) -> Result<(), UiError> {
Ok(()) Ok(())
} }
async fn do_jwt_stuff(dispatch: &WebUiDispatcher) -> Result<(), UiError> {
let jwt = api::auth::fetch_jwt().await?;
dispatch.send(Action::UpdateJwt(jwt));
Ok(())
}
impl Component for YewduxRoomList { impl Component for YewduxRoomList {
type Message = (); type Message = ();
type Properties = WebUiDispatcher; type Properties = WebUiDispatcher;
@ -72,9 +78,19 @@ impl Component for YewduxRoomList {
fn view(&self) -> Html { fn view(&self) -> Html {
let the_future = self.link.callback(move |_| {}); let the_future = self.link.callback(move |_| {});
let dispatch = Arc::new(self.dispatch.clone());
let jwt_update = self.link.callback(move |_| {
let dispatch = dispatch.clone();
spawn_local(async move {
do_jwt_stuff(&*dispatch).await;
});
});
html! { html! {
<div> <div>
<button onclick=the_future>{ "Add Room" }</button> <button onclick=the_future>{ "Add Room" }</button>
<button onclick=jwt_update>{ "Fetch JWT" }</button>
<div> { "Current JWT: " } { self.dispatch.state().jwt_token.as_ref().unwrap_or(&"[not set]".to_string()) }</div>
<ul> <ul>
{ {
for self.dispatch.state().rooms.iter().map(|room| { for self.dispatch.state().rooms.iter().map(|room| {

View File

@ -8,10 +8,12 @@ pub(crate) struct Room {
#[derive(Default, Clone)] #[derive(Default, Clone)]
pub(crate) struct WebUiState { pub(crate) struct WebUiState {
pub jwt_token: Option<String>,
pub rooms: Vec<Room>, pub rooms: Vec<Room>,
} }
pub(crate) enum Action { pub(crate) enum Action {
UpdateJwt(String),
AddRoom(Room), AddRoom(Room),
} }
@ -19,17 +21,20 @@ impl Reducer for WebUiState {
type Action = Action; type Action = Action;
fn new() -> Self { fn new() -> Self {
Self { rooms: vec![] } Self {
jwt_token: None,
rooms: vec![],
}
} }
fn reduce(&mut self, action: Self::Action) -> bool { fn reduce(&mut self, action: Self::Action) -> bool {
match action { match action {
Action::AddRoom(room) => { Action::UpdateJwt(jwt_token) => self.jwt_token = Some(jwt_token),
self.rooms.push(room.clone()); Action::AddRoom(room) => self.rooms.push(room.clone()),
};
true true
} }
}
}
} }
pub(crate) type WebUiDispatcher = DispatchProps<ReducerStore<WebUiState>>; pub(crate) type WebUiDispatcher = DispatchProps<ReducerStore<WebUiState>>;