From 2de589d05cbb108216d39d1b80f942873654388a Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Thu, 13 May 2021 19:33:57 +0200 Subject: [PATCH] app: implement login and authorization --- app/Cargo.toml | 12 +++-- app/src/api.rs | 107 +++++++++++++++++++++++++++++++++--------- app/src/app.rs | 58 ++++++++++++++++++++--- app/src/lib.rs | 1 + app/src/login.rs | 103 ++++++++++++++++++++++++++++++++++++++++ app/src/user_table.rs | 8 +--- 6 files changed, 249 insertions(+), 40 deletions(-) create mode 100644 app/src/login.rs diff --git a/app/Cargo.toml b/app/Cargo.toml index ea7014a..9ca7d58 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -5,12 +5,16 @@ authors = ["Valentin Tolmer ", "Steve Barrau FetchOptions { + FetchOptions { + credentials: Some(Credentials::SameOrigin), + ..FetchOptions::default() + } +} + +fn get_claims_from_jwt(jwt: &str) -> Result { + use jwt::*; + let token = Token::::parse_unverified(jwt)?; + Ok(token.claims().clone()) +} + impl HostService { pub fn list_users( - &mut self, request: ListUsersRequest, callback: Callback>>, ) -> Result { - let url = format!("/api/users"); - let handler = - move |response: Response>| { - let (meta, maybe_data) = response.into_parts(); - match maybe_data { - Ok(data) => { - if meta.status.is_success() { - callback.emit(serde_json::from_str(&data).map_err(|e| { - anyhow::format_err!("Could not parse response: {}", e) - })) - } else { - callback.emit(Err(anyhow::anyhow!("[{}]: {}", meta.status, data))) - } + let url = "/api/users"; + let handler = move |response: Response>| { + let (meta, maybe_data) = response.into_parts(); + match maybe_data { + Ok(data) => { + if meta.status.is_success() { + callback.emit( + serde_json::from_str(&data) + .map_err(|e| anyhow!("Could not parse response: {}", e)), + ) + } else { + callback.emit(Err(anyhow!("[{}]: {}", meta.status, data))) } - Err(e) => callback.emit(Err(anyhow::anyhow!("Could not fetch: {}", e))), } - }; - let request = Request::post(url.as_str()) + Err(e) => callback.emit(Err(anyhow!("Could not fetch: {}", e))), + } + }; + let request = Request::post(url) .header("Content-Type", "application/json") - .body(Json(&request)) - .unwrap(); - FetchService::fetch(request, handler.into()) + .body(Json(&request))?; + FetchService::fetch_with_options(request, get_default_options(), handler.into()) + } + + pub fn authenticate( + request: BindRequest, + callback: Callback>, + ) -> Result { + let url = "/api/authorize"; + let handler = move |response: Response>| { + let (meta, maybe_data) = response.into_parts(); + match maybe_data { + Ok(data) => { + if meta.status.is_success() { + match get_claims_from_jwt(&data) { + Ok(jwt_claims) => { + let document = web_sys::window() + .unwrap() + .document() + .unwrap() + .dyn_into::() + .unwrap(); + document + .set_cookie(&format!( + "user_id={}; expires={}", + &jwt_claims.user, &jwt_claims.exp + )) + .unwrap(); + callback.emit(Ok(jwt_claims.user.clone())) + } + Err(e) => { + callback.emit(Err(anyhow!("Could not parse response: {}", e))) + } + } + } else if meta.status == 401 { + callback.emit(Err(anyhow!("Invalid username or password"))) + } else { + callback.emit(Err(anyhow!( + "Could not authenticate: [{}]: {}", + meta.status, + data + ))) + } + } + Err(e) => { + callback.emit(Err(anyhow!("Could not reach authentication server: {}", e))) + } + } + }; + let request = Request::post(url) + .header("Content-Type", "application/json") + .body(Json(&request))?; + FetchService::fetch_with_options(request, get_default_options(), handler.into()) } } diff --git a/app/src/app.rs b/app/src/app.rs index 0cb046a..ba9015d 100644 --- a/app/src/app.rs +++ b/app/src/app.rs @@ -1,20 +1,60 @@ +use crate::login::LoginForm; use crate::user_table::UserTable; +use anyhow::{anyhow, Result}; +use wasm_bindgen::JsCast; use yew::prelude::*; -pub struct App {} +pub struct App { + link: ComponentLink, + user_name: Option, +} -pub enum Msg {} +pub enum Msg { + Login(String), +} + +fn extract_user_id_cookie() -> Result { + let document = web_sys::window() + .unwrap() + .document() + .unwrap() + .dyn_into::() + .unwrap(); + let cookies = document.cookie().unwrap(); + yew::services::ConsoleService::info(&cookies); + cookies + .split(";") + .filter_map(|c| c.split_once('=')) + .map(|(name, value)| { + if name == "user_id" { + Ok(value.into()) + } else { + Err(anyhow!("Wrong cookie")) + } + }) + .filter(Result::is_ok) + .next() + .unwrap_or(Err(anyhow!("User ID cookie not found in response"))) +} impl Component for App { type Message = Msg; type Properties = (); - fn create(_: Self::Properties, _link: ComponentLink) -> Self { - App {} + fn create(_: Self::Properties, link: ComponentLink) -> Self { + App { + link: link.clone(), + user_name: extract_user_id_cookie().ok(), + } } - fn update(&mut self, _msg: Self::Message) -> ShouldRender { - false + fn update(&mut self, msg: Self::Message) -> ShouldRender { + match msg { + Msg::Login(user_name) => { + self.user_name = Some(user_name); + } + } + true } fn change(&mut self, _: Self::Properties) -> ShouldRender { @@ -25,7 +65,11 @@ impl Component for App { html! {

{ "LLDAP" }

- + {if self.user_name.is_some() { + html! {} + } else { + html! {} + }}
} } diff --git a/app/src/lib.rs b/app/src/lib.rs index c7192b4..f883baa 100644 --- a/app/src/lib.rs +++ b/app/src/lib.rs @@ -1,6 +1,7 @@ #![recursion_limit = "256"] mod api; mod app; +mod login; mod user_table; use wasm_bindgen::prelude::{wasm_bindgen, JsValue}; diff --git a/app/src/login.rs b/app/src/login.rs new file mode 100644 index 0000000..9fa761d --- /dev/null +++ b/app/src/login.rs @@ -0,0 +1,103 @@ +use crate::api::HostService; +use anyhow::{anyhow, Result}; +use lldap_model::*; +use wasm_bindgen::JsCast; +use yew::prelude::*; +use yew::services::fetch::FetchTask; + +pub struct LoginForm { + link: ComponentLink, + on_logged_in: Callback, + error: Option, + node_ref: NodeRef, + task: Option, +} + +#[derive(Clone, PartialEq, Properties)] +pub struct Props { + pub on_logged_in: Callback, +} + +pub enum Msg { + Submit, + AuthenticationResponse(Result), +} + +impl Component for LoginForm { + type Message = Msg; + type Properties = Props; + + fn create(props: Self::Properties, link: ComponentLink) -> Self { + LoginForm { + link: link.clone(), + on_logged_in: props.on_logged_in, + error: None, + node_ref: NodeRef::default(), + task: None, + } + } + + fn update(&mut self, msg: Self::Message) -> ShouldRender { + match msg { + Msg::Submit => { + let document = web_sys::window().unwrap().document().unwrap(); + let username = document + .get_element_by_id("username") + .unwrap() + .dyn_into::() + .unwrap() + .value(); + let password = document + .get_element_by_id("password") + .unwrap() + .dyn_into::() + .unwrap() + .value(); + let req = BindRequest { + name: username.to_string(), + password: password.to_string(), + }; + match HostService::authenticate( + req, + self.link.callback(Msg::AuthenticationResponse), + ) { + Ok(task) => self.task = Some(task), + Err(e) => self.error = Some(e), + } + } + Msg::AuthenticationResponse(Ok(user_id)) => { + self.on_logged_in.emit(user_id); + } + Msg::AuthenticationResponse(Err(e)) => { + self.error = Some(anyhow!("Could not log in: {}", e)); + } + }; + true + } + + fn change(&mut self, _: Self::Properties) -> ShouldRender { + false + } + + fn view(&self) -> Html { + html! { +
+
+ + +
+
+ + +
+ +
+ { if let Some(e) = &self.error { + html! { e.to_string() } + } else { html! {} } + } +
+
+ } + } +} diff --git a/app/src/user_table.rs b/app/src/user_table.rs index 7fac30a..2d5e1f3 100644 --- a/app/src/user_table.rs +++ b/app/src/user_table.rs @@ -7,7 +7,6 @@ use yew::services::{fetch::FetchTask, ConsoleService}; pub struct UserTable { link: ComponentLink, - ipservice: HostService, task: Option, users: Option>>, } @@ -18,10 +17,7 @@ pub enum Msg { impl UserTable { fn get_users(&mut self, req: ListUsersRequest) { - match self - .ipservice - .list_users(req, self.link.callback(Msg::ListUsersResponse)) - { + match HostService::list_users(req, self.link.callback(Msg::ListUsersResponse)) { Ok(task) => self.task = Some(task), Err(e) => { self.task = None; @@ -38,7 +34,6 @@ impl Component for UserTable { fn create(_: Self::Properties, link: ComponentLink) -> Self { let mut table = UserTable { link: link.clone(), - ipservice: HostService::default(), task: None, users: None, }; @@ -54,7 +49,6 @@ impl Component for UserTable { true } Msg::ListUsersResponse(Err(e)) => { - self.task = None; self.users = Some(Err(anyhow!("Error listing users: {}", e))); true }