feat: add unix passwod reset to security web ui (#1014)

* feat: add unix passwod reset to security web ui
* refactor: fetch profile info in ViewsApp
prevents constant re-fetching of the profile page and allows every view
to access the current_user property
* refactor: move unix password change to component
* docs: add @theSuess to contributors
* fix: further specify kind of password updated
* refactor: perform validity check before submit
* chore: regenerate vendored wasm package
This commit is contained in:
Dominik Süß 2022-09-07 03:40:54 +02:00 committed by GitHub
parent b793ec6e79
commit 8416069c61
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 526 additions and 310 deletions

View file

@ -18,6 +18,7 @@
* Kellin (kellinm)
* Carla Schroder (cjschroder)
* Thomas Sanchez (daedric)
* Dominik Süß (theSuess)
## Acknowledgements

View file

@ -324,7 +324,7 @@ impl fmt::Display for AuthType {
///
/// It's likely that this must have a relationship to the server's user structure
/// and to the Entry so that filters or access controls can be applied.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub struct UserAuthToken {
pub session_id: Uuid,
@ -1023,7 +1023,7 @@ impl WhoamiRequest {
}
*/
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct WhoamiResponse {
// Should we just embed the entry? Or destructure it?
pub youare: Entry,

View file

@ -50,9 +50,11 @@ features = [
"Element",
"Event",
"FocusEvent",
"FormData",
"Headers",
"HtmlButtonElement",
"HtmlDocument",
"HtmlFormElement",
"Navigator",
"PublicKeyCredential",
"PublicKeyCredentialCreationOptions",

View file

@ -224,7 +224,7 @@ function addBorrowedObject(obj) {
}
function __wbg_adapter_30(arg0, arg1, arg2) {
try {
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h059700c1260af691(arg0, arg1, addBorrowedObject(arg2));
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hea78f67aa49cdca6(arg0, arg1, addBorrowedObject(arg2));
} finally {
heap[stack_pointer++] = undefined;
}
@ -252,11 +252,11 @@ function makeClosure(arg0, arg1, dtor, f) {
return real;
}
function __wbg_adapter_33(arg0, arg1, arg2) {
wasm._dyn_core__ops__function__Fn__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hba0e2d91e3f361c8(arg0, arg1, addHeapObject(arg2));
wasm._dyn_core__ops__function__Fn__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h5d377d592c5210bc(arg0, arg1, addHeapObject(arg2));
}
function __wbg_adapter_36(arg0, arg1, arg2) {
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hd75fa9c9bfc3f75e(arg0, arg1, addHeapObject(arg2));
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h5f33dcf69dc6b3de(arg0, arg1, addHeapObject(arg2));
}
/**
@ -380,17 +380,13 @@ function getImports() {
const ret = typeof(v) === 'boolean' ? (v ? 1 : 0) : 2;
return ret;
};
imports.wbg.__wbindgen_is_undefined = function(arg0) {
const ret = getObject(arg0) === undefined;
return ret;
};
imports.wbg.__wbindgen_json_parse = function(arg0, arg1) {
const ret = JSON.parse(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
};
imports.wbg.__wbindgen_number_new = function(arg0) {
const ret = arg0;
return addHeapObject(ret);
imports.wbg.__wbindgen_is_undefined = function(arg0) {
const ret = getObject(arg0) === undefined;
return ret;
};
imports.wbg.__wbindgen_number_get = function(arg0, arg1) {
const obj = getObject(arg1);
@ -398,6 +394,10 @@ function getImports() {
getFloat64Memory0()[arg0 / 8 + 1] = isLikeNone(ret) ? 0 : ret;
getInt32Memory0()[arg0 / 4 + 0] = !isLikeNone(ret);
};
imports.wbg.__wbindgen_number_new = function(arg0) {
const ret = arg0;
return addHeapObject(ret);
};
imports.wbg.__wbg_new_693216e109162396 = function() {
const ret = new Error();
return addHeapObject(ret);
@ -416,17 +416,17 @@ function getImports() {
wasm.__wbindgen_free(arg0, arg1);
}
};
imports.wbg.__wbg_debug_5a27eb2cb0d074ba = function(arg0, arg1) {
imports.wbg.__wbg_debug_f058a17401150b46 = function(arg0, arg1) {
var v0 = getArrayJsValueFromWasm0(arg0, arg1).slice();
wasm.__wbindgen_free(arg0, arg1 * 4);
console.debug(...v0);
};
imports.wbg.__wbg_error_1a35d3879f286b52 = function(arg0, arg1) {
imports.wbg.__wbg_error_726977b4dd084e24 = function(arg0, arg1) {
var v0 = getArrayJsValueFromWasm0(arg0, arg1).slice();
wasm.__wbindgen_free(arg0, arg1 * 4);
console.error(...v0);
};
imports.wbg.__wbg_warn_2aa0e7178e1d35f6 = function(arg0, arg1) {
imports.wbg.__wbg_warn_921059440157e870 = function(arg0, arg1) {
var v0 = getArrayJsValueFromWasm0(arg0, arg1).slice();
wasm.__wbindgen_free(arg0, arg1 * 4);
console.warn(...v0);
@ -487,6 +487,30 @@ function getImports() {
const ret = getObject(arg0).querySelector(getStringFromWasm0(arg1, arg2));
return isLikeNone(ret) ? 0 : addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_value_eb32f706ae6bfab2 = function(arg0, arg1) {
const ret = getObject(arg1).value;
const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
};
imports.wbg.__wbg_setvalue_3dd349be116107ce = function(arg0, arg1, arg2) {
getObject(arg0).value = getStringFromWasm0(arg1, arg2);
};
imports.wbg.__wbg_get_aab8f8a9b87125ad = function() { return handleError(function (arg0, arg1, arg2, arg3) {
const ret = getObject(arg1).get(getStringFromWasm0(arg2, arg3));
var ptr0 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
}, arguments) };
imports.wbg.__wbg_set_b5c36262f65fae92 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).set(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
}, arguments) };
imports.wbg.__wbg_instanceof_HtmlFormElement_cf35f564c31c7e40 = function(arg0) {
const ret = getObject(arg0) instanceof HTMLFormElement;
return ret;
};
imports.wbg.__wbg_getItem_1db55b1eb4116c1e = function() { return handleError(function (arg0, arg1, arg2, arg3) {
const ret = getObject(arg1).getItem(getStringFromWasm0(arg2, arg3));
var ptr0 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
@ -500,66 +524,6 @@ function getImports() {
imports.wbg.__wbg_setItem_1f474989a35f4c9f = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).setItem(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
}, arguments) };
imports.wbg.__wbg_get_aab8f8a9b87125ad = function() { return handleError(function (arg0, arg1, arg2, arg3) {
const ret = getObject(arg1).get(getStringFromWasm0(arg2, arg3));
var ptr0 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
}, arguments) };
imports.wbg.__wbg_set_b5c36262f65fae92 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).set(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
}, arguments) };
imports.wbg.__wbg_pathname_8ed2fc02f98aeaaf = function(arg0, arg1) {
const ret = getObject(arg1).pathname;
const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
};
imports.wbg.__wbg_new_d1d1300265e34170 = function() { return handleError(function (arg0, arg1) {
const ret = new URL(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_value_eb32f706ae6bfab2 = function(arg0, arg1) {
const ret = getObject(arg1).value;
const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
};
imports.wbg.__wbg_setvalue_3dd349be116107ce = function(arg0, arg1, arg2) {
getObject(arg0).value = getStringFromWasm0(arg1, arg2);
};
imports.wbg.__wbg_headers_0aeca08d4e61e2e7 = function(arg0) {
const ret = getObject(arg0).headers;
return addHeapObject(ret);
};
imports.wbg.__wbg_newwithstrandinit_de7c409ec8538105 = function() { return handleError(function (arg0, arg1, arg2) {
const ret = new Request(getStringFromWasm0(arg0, arg1), getObject(arg2));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_add_a1fa1336c6b306df = function() { return handleError(function (arg0, arg1, arg2) {
getObject(arg0).add(getStringFromWasm0(arg1, arg2));
}, arguments) };
imports.wbg.__wbg_remove_dce5eca3c9fcea70 = function() { return handleError(function (arg0, arg1, arg2) {
getObject(arg0).remove(getStringFromWasm0(arg1, arg2));
}, arguments) };
imports.wbg.__wbg_create_2c518207ce8a6157 = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg0).create(getObject(arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_get_5ec74cdfbbefe775 = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg0).get(getObject(arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_href_cae04ee9562fc683 = function(arg0, arg1) {
const ret = getObject(arg1).href;
const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
};
imports.wbg.__wbg_instanceof_HtmlInputElement_3fad42774bc62388 = function(arg0) {
const ret = getObject(arg0) instanceof HTMLInputElement;
return ret;
@ -577,6 +541,46 @@ function getImports() {
imports.wbg.__wbg_setvalue_7b7950dacc5eb607 = function(arg0, arg1, arg2) {
getObject(arg0).value = getStringFromWasm0(arg1, arg2);
};
imports.wbg.__wbg_add_a1fa1336c6b306df = function() { return handleError(function (arg0, arg1, arg2) {
getObject(arg0).add(getStringFromWasm0(arg1, arg2));
}, arguments) };
imports.wbg.__wbg_remove_dce5eca3c9fcea70 = function() { return handleError(function (arg0, arg1, arg2) {
getObject(arg0).remove(getStringFromWasm0(arg1, arg2));
}, arguments) };
imports.wbg.__wbg_pathname_8ed2fc02f98aeaaf = function(arg0, arg1) {
const ret = getObject(arg1).pathname;
const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
};
imports.wbg.__wbg_new_d1d1300265e34170 = function() { return handleError(function (arg0, arg1) {
const ret = new URL(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_create_2c518207ce8a6157 = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg0).create(getObject(arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_get_5ec74cdfbbefe775 = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg0).get(getObject(arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_href_cae04ee9562fc683 = function(arg0, arg1) {
const ret = getObject(arg1).href;
const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
};
imports.wbg.__wbg_headers_0aeca08d4e61e2e7 = function(arg0) {
const ret = getObject(arg0).headers;
return addHeapObject(ret);
};
imports.wbg.__wbg_newwithstrandinit_de7c409ec8538105 = function() { return handleError(function (arg0, arg1, arg2) {
const ret = new Request(getStringFromWasm0(arg0, arg1), getObject(arg2));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_instanceof_Element_1714e50f9bda1d15 = function(arg0) {
const ret = getObject(arg0) instanceof Element;
return ret;
@ -608,6 +612,14 @@ function getImports() {
imports.wbg.__wbg_focus_66bb7c767837cd51 = function() { return handleError(function (arg0) {
getObject(arg0).focus();
}, arguments) };
imports.wbg.__wbg_credentials_c3a25c2c25bfa304 = function(arg0) {
const ret = getObject(arg0).credentials;
return addHeapObject(ret);
};
imports.wbg.__wbg_getClientExtensionResults_93d06fddc73f65f9 = function(arg0) {
const ret = getObject(arg0).getClientExtensionResults();
return addHeapObject(ret);
};
imports.wbg.__wbg_instanceof_Event_8c74064684d03e14 = function(arg0) {
const ret = getObject(arg0) instanceof Event;
return ret;
@ -623,12 +635,12 @@ function getImports() {
imports.wbg.__wbg_preventDefault_b4d36c8196fbe491 = function(arg0) {
getObject(arg0).preventDefault();
};
imports.wbg.__wbg_credentials_c3a25c2c25bfa304 = function(arg0) {
const ret = getObject(arg0).credentials;
imports.wbg.__wbg_newwithform_e0bf0bf59a04cc42 = function() { return handleError(function (arg0) {
const ret = new FormData(getObject(arg0));
return addHeapObject(ret);
};
imports.wbg.__wbg_getClientExtensionResults_93d06fddc73f65f9 = function(arg0) {
const ret = getObject(arg0).getClientExtensionResults();
}, arguments) };
imports.wbg.__wbg_get_7d2187aabf8b90b6 = function(arg0, arg1, arg2) {
const ret = getObject(arg0).get(getStringFromWasm0(arg1, arg2));
return addHeapObject(ret);
};
imports.wbg.__wbg_addEventListener_ec92ea1297eefdfc = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
@ -813,16 +825,16 @@ function getImports() {
const ret = wasm.memory;
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper4903 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1268, __wbg_adapter_30);
imports.wbg.__wbindgen_closure_wrapper4401 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1059, __wbg_adapter_30);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper5008 = function(arg0, arg1, arg2) {
const ret = makeClosure(arg0, arg1, 1301, __wbg_adapter_33);
imports.wbg.__wbindgen_closure_wrapper4577 = function(arg0, arg1, arg2) {
const ret = makeClosure(arg0, arg1, 1084, __wbg_adapter_33);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper5273 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1350, __wbg_adapter_36);
imports.wbg.__wbindgen_closure_wrapper4718 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1120, __wbg_adapter_36);
return addHeapObject(ret);
};

View file

@ -0,0 +1,302 @@
use compact_jwt::{Jws, JwsUnverified};
use kanidm_proto::v1::{SingleStringRequest, UserAuthToken};
use std::str::FromStr;
use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt};
use wasm_bindgen_futures::JsFuture;
use web_sys::{FormData, HtmlFormElement};
use web_sys::{Request, RequestInit, RequestMode, Response};
use yew::prelude::*;
use crate::error::*;
use crate::utils;
#[derive(PartialEq)]
enum PwCheck {
Init,
Valid,
Invalid,
}
pub struct ChangeUnixPassword {
state: State,
pw_check: PwCheck,
pw_val: String,
pw_check_val: String,
}
#[derive(Debug, Default)]
struct FormValues {
password_input: String,
password_repeat_input: String,
}
impl From<FormData> for FormValues {
fn from(data: FormData) -> Self {
Self {
password_input: data.get("password_input").as_string().unwrap(),
password_repeat_input: data.get("password_repeat_input").as_string().unwrap(),
}
}
}
pub enum Msg {
Submit(FormData),
Error { emsg: String, kopid: Option<String> },
Success,
PasswordCheck,
}
impl From<FetchError> for Msg {
fn from(fe: FetchError) -> Self {
Msg::Error {
emsg: fe.as_string(),
kopid: None,
}
}
}
pub enum State {
Init,
Error { emsg: String, kopid: Option<String> },
}
#[derive(PartialEq, Eq, Properties)]
pub struct ChangeUnixPasswordProps {
pub token: String,
}
impl Component for ChangeUnixPassword {
type Message = Msg;
type Properties = ChangeUnixPasswordProps;
fn create(_ctx: &Context<Self>) -> Self {
Self {
state: State::Init,
pw_check: PwCheck::Init,
pw_val: "".to_string(),
pw_check_val: "".to_string(),
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::Submit(data) => {
let fd: FormValues = data.into();
if fd.password_input != fd.password_repeat_input {
return self.update(
ctx,
Msg::Error {
emsg: "Password fields did not match".to_string(),
kopid: None,
},
);
}
let tk = ctx.props().token.clone();
ctx.link().send_future(async {
match Self::update_unix_password(tk, fd.password_input).await {
Ok(v) => v,
Err(v) => v.into(),
}
});
false
}
Msg::Error { emsg, kopid } => {
self.reset();
self.state = State::Error { emsg, kopid };
self.pw_check = PwCheck::Init;
true
}
Msg::Success => {
self.reset();
utils::modal_hide_by_id(crate::constants::ID_UNIX_PASSWORDCHANGE);
self.state = State::Init;
true
}
Msg::PasswordCheck => {
let pw = utils::get_value_from_element_id("password_input")
.unwrap_or_else(|| "".to_string());
let check = utils::get_value_from_element_id("password_repeat_input")
.unwrap_or_else(|| "".to_string());
if pw == check {
self.pw_check = PwCheck::Valid
} else {
self.pw_check = PwCheck::Invalid
}
self.pw_val = pw;
self.pw_check_val = check;
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let flash = match &self.state {
State::Error { emsg, kopid } => {
let message = match kopid {
Some(k) => format!("An error occured - {} - {}", emsg, k),
None => format!("An error occured - {} - No Operation ID", emsg),
};
html! {
<div class="alert alert-danger alert-dismissible fade show" role="alert">
{ message }
<button type="button" class="btn btn-close" data-dismiss="alert" aria-label="Close"></button>
</div>
}
}
_ => html! { <></> },
};
let submit_enabled = self.pw_check == PwCheck::Valid;
let pw_val = self.pw_val.clone();
let pw_check_val = self.pw_check_val.clone();
let pw_check_class = match &self.pw_check {
PwCheck::Init | PwCheck::Valid => classes!("form-control"),
PwCheck::Invalid => classes!("form-control", "is-invalid"),
};
html! {
<>
<button type="button" class="btn btn-primary"
data-bs-toggle="modal"
data-bs-target={format!("#{}", crate::constants::ID_UNIX_PASSWORDCHANGE)}
>
{ "Update your Unix Password" }
</button>
<div class="modal" tabindex="-1" role="dialog" id={crate::constants::ID_UNIX_PASSWORDCHANGE}>
<div class="modal-dialog" role="document">
<form
onsubmit={
ctx.link().callback(|e: FocusEvent| {
e.prevent_default();
let form = e.target().and_then(|t| t.dyn_into::<HtmlFormElement>().ok()).unwrap();
Msg::Submit(FormData::new_with_form(&form).unwrap())
})
}
>
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{"Update your unix password"}</h5>
</div>
<div class="modal-body">
<p> { "This password is used when logging into a unix-like system as well as applications utilizing LDAP" } </p>
{ flash }
<div class="form-group">
<label for="password_input"> {"New Password" }</label>
<input
autofocus=true
class="autofocus form-control"
name="password_input"
id="password_input"
type="password"
value={ pw_val }
oninput={
ctx.link()
.callback(move |_| {
Msg::PasswordCheck
})
}
/>
</div>
<div class="form-group">
<label for="password_repeat_input"> {"Repeat Password" }</label>
<input
class={ pw_check_class }
name="password_repeat_input"
id="password_repeat_input"
type="password"
value={ pw_check_val }
oninput={
ctx.link()
.callback(move |_| {
Msg::PasswordCheck
})
}
/>
<div class="invalid-feedback">
{ "Passwords do not match." }
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-success" disabled={ !submit_enabled }>{ "Update Password" }</button>
<button type="button" class="btn btn-secondary"
onclick={
ctx.link().callback(|_e| {
Msg::Success
})
}
>{"Cancel"}</button>
</div>
</div>
</form>
</div>
</div>
</>
}
}
fn changed(&mut self, _ctx: &Context<Self>) -> bool {
false
}
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {}
fn destroy(&mut self, _ctx: &Context<Self>) {}
}
impl ChangeUnixPassword {
async fn update_unix_password(token: String, new_password: String) -> Result<Msg, FetchError> {
let jwtu = JwsUnverified::from_str(&token).expect_throw("Invalid UAT, unable to parse");
let uat: Jws<UserAuthToken> = jwtu
.unsafe_release_without_verification()
.expect_throw("Unvalid UAT, unable to release ");
let id = uat.inner.uuid.to_string();
let changereq_jsvalue = serde_json::to_string(&SingleStringRequest {
value: new_password,
})
.map(|s| JsValue::from(&s))
.expect_throw("Failed to change request");
let mut opts = RequestInit::new();
opts.method("PUT");
opts.mode(RequestMode::SameOrigin);
opts.body(Some(&changereq_jsvalue));
let uri = format!("/v1/person/{}/_unix/_credential", id);
let request = Request::new_with_str_and_init(uri.as_str(), &opts)?;
request
.headers()
.set("content-type", "application/json")
.expect_throw("failed to set header");
request
.headers()
.set("authorization", format!("Bearer {}", token).as_str())
.expect_throw("failed to set header");
let window = utils::window();
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
let resp: Response = resp_value.dyn_into().expect_throw("Invalid response type");
let status = resp.status();
if status == 200 {
Ok(Msg::Success)
} else {
let headers = resp.headers();
let kopid = headers.get("x-kanidm-opid").ok().flatten();
let text = JsFuture::from(resp.text()?).await?;
let emsg = text.as_string().unwrap_or_else(|| "".to_string());
Ok(Msg::Error { emsg, kopid })
}
}
fn reset(&mut self) {
self.pw_val = "".to_string();
self.pw_check_val = "".to_string();
self.pw_check = PwCheck::Init;
}
}

View file

@ -0,0 +1 @@
pub mod change_unix_password;

View file

@ -6,6 +6,8 @@ pub const CSS_CLASSES_BODY_FORM: &[&str] = &["flex-column", "d-flex", "h-100"];
// the HTML element ID that the signout modal dialogue box has
pub const ID_SIGNOUTMODAL: &str = "signoutModal";
// the HTML element ID that the unix password dialog box has
pub const ID_UNIX_PASSWORDCHANGE: &str = "unixPasswordModal";
// classes for buttons
pub const CLASS_BUTTON_DARK: &str = "btn btn-dark";
pub const CLASS_BUTTON_SUCCESS: &str = "btn btn-success";

View file

@ -301,6 +301,11 @@ impl Component for PwModalApp {
type="password"
value={ pw_check_val }
/>
if !submit_enabled {
<div class="invalid-feedback">
{ "Passwords do not match." }
</div>
}
</form>
</div>
<div class="modal-footer">

View file

@ -29,6 +29,8 @@ mod oauth2;
mod utils;
mod views;
mod components;
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
pub fn run_app() -> Result<(), JsValue> {
yew::start_app::<manager::ManagerApp>();

View file

@ -1,13 +1,14 @@
use crate::error::*;
use crate::models;
use crate::utils;
#[cfg(debug)]
use gloo::console;
use yew::prelude::*;
use crate::manager::Route;
use yew_router::prelude::*;
use kanidm_proto::v1::WhoamiResponse;
use serde::{Deserialize, Serialize};
use wasm_bindgen::{JsCast, UnwrapThrowExt};
@ -49,15 +50,18 @@ enum State {
#[derive(PartialEq, Eq, Properties)]
pub struct ViewProps {
pub token: String,
pub current_user: Option<WhoamiResponse>,
}
pub struct ViewsApp {
state: State,
current_user: Option<WhoamiResponse>,
}
pub enum ViewsMsg {
Verified(String),
Logout,
ProfileInfoRecieved(WhoamiResponse),
Error { emsg: String, kopid: Option<String> },
}
@ -70,24 +74,6 @@ impl From<FetchError> for ViewsMsg {
}
}
fn switch(route: &ViewRoute) -> Html {
#[cfg(debug)]
console::debug!("views::switch");
// safety - can't panic because to get to this location we MUST be authenticated!
let token =
models::get_bearer_token().expect_throw("Invalid state, bearer token must be present!");
match route {
ViewRoute::Apps => html! { <AppsApp /> },
ViewRoute::Profile => html! { <ProfileApp token={ token } /> },
ViewRoute::Security => html! { <SecurityApp token={ token } /> },
ViewRoute::NotFound => html! {
<Redirect<Route> to={Route::NotFound}/>
},
}
}
impl Component for ViewsApp {
type Message = ViewsMsg;
type Properties = ();
@ -114,7 +100,10 @@ impl Component for ViewsApp {
None => State::LoginRequired,
};
ViewsApp { state }
ViewsApp {
state,
current_user: None,
}
}
fn changed(&mut self, _ctx: &Context<Self>) -> bool {
@ -123,12 +112,20 @@ impl Component for ViewsApp {
false
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
#[cfg(debug)]
console::debug!("views::update");
match msg {
ViewsMsg::Verified(token) => {
let tk = token.clone();
self.state = State::Authenticated(token);
// Populate the user profile
ctx.link().send_future(async {
match Self::fetch_user_data(tk).await {
Ok(v) => v,
Err(v) => v.into(),
}
});
true
}
ViewsMsg::Logout => {
@ -136,6 +133,10 @@ impl Component for ViewsApp {
self.state = State::LoginRequired;
true
}
ViewsMsg::ProfileInfoRecieved(profile) => {
self.current_user = Some(profile);
true
}
ViewsMsg::Error { emsg, kopid } => {
self.state = State::Error { emsg, kopid };
true
@ -207,6 +208,8 @@ impl Component for ViewsApp {
impl ViewsApp {
/// The base page for the user dashboard
fn view_authenticated(&self, ctx: &Context<Self>) -> Html {
let current_user = self.current_user.clone();
// WARN set dash-body against body here?
html! {
<>
@ -278,12 +281,24 @@ impl ViewsApp {
</div>
</div>
<main class="p-3 x-auto">
<Switch<ViewRoute> render={ Switch::render(switch) } />
<Switch<ViewRoute> render={ Switch::render(move |route: &ViewRoute| {
// safety - can't panic because to get to this location we MUST be authenticated!
let token =
models::get_bearer_token().expect_throw("Invalid state, bearer token must be present!");
match route {
ViewRoute::Apps => html! { <AppsApp /> },
ViewRoute::Profile => html! { <ProfileApp token={ token } current_user={ current_user.clone() } /> },
ViewRoute::Security => html! { <SecurityApp token={ token } current_user={ current_user.clone() } /> },
ViewRoute::NotFound => html! {
<Redirect<Route> to={Route::NotFound}/>
},
}
})} />
</main>
</>
}
}
async fn check_token_valid(token: String) -> Result<ViewsMsg, FetchError> {
let mut opts = RequestInit::new();
opts.method("GET");
@ -318,4 +333,42 @@ impl ViewsApp {
Ok(ViewsMsg::Error { emsg, kopid })
}
}
async fn fetch_user_data(token: String) -> Result<ViewsMsg, FetchError> {
let mut opts = RequestInit::new();
opts.method("GET");
opts.mode(RequestMode::SameOrigin);
let request = Request::new_with_str_and_init("/v1/self", &opts)?;
request
.headers()
.set("content-type", "application/json")
.expect_throw("failed to set header");
request
.headers()
.set("authorization", format!("Bearer {}", token).as_str())
.expect_throw("failed to set header");
let window = utils::window();
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
let resp: Response = resp_value.dyn_into().expect_throw("Invalid response type");
let status = resp.status();
let headers = resp.headers();
let kopid = headers.get("x-kanidm-opid").ok().flatten();
if status == 200 {
let jsval = JsFuture::from(resp.json()?).await?;
let whoamiresponse: WhoamiResponse = jsval
.into_serde()
.map_err(|e| {
let e_msg = format!("serde error getting user data -> {:?}", e);
console::error!(e_msg.as_str());
})
.expect_throw("Invalid response type");
Ok(ViewsMsg::ProfileInfoRecieved(whoamiresponse))
} else {
let text = JsFuture::from(resp.text()?).await?;
let emsg = text.as_string().unwrap_or_else(|| "".to_string());
Ok(ViewsMsg::Error { emsg, kopid })
}
}
}

View file

@ -1,130 +1,35 @@
use crate::error::FetchError;
// use crate::error::*;
// use crate::models;
use crate::utils;
use crate::views::ViewProps;
// use compact_jwt::{Jws, JwsUnverified};
use gloo::console;
use kanidm_proto::v1::WhoamiResponse;
// use kanidm_proto::v1::{UserAuthToken,WhoamiResponse};
use std::fmt::Debug;
// use std::str::FromStr;
use wasm_bindgen::JsCast;
// use wasm_bindgen::JsValue;
use wasm_bindgen::UnwrapThrowExt;
use wasm_bindgen_futures::JsFuture;
use web_sys::{Request, RequestInit, RequestMode, Response};
use yew::prelude::*;
// use web_sys::{Request, RequestInit, RequestMode, Response};
pub enum Msg {
TokenValid(String),
TokenInvalid,
Error { emsg: String, kopid: Option<String> },
ProfileInfoRecieved(WhoamiResponse),
}
#[derive(Debug)]
enum ProfileAppState {
Loading,
Loaded,
}
impl From<FetchError> for Msg {
fn from(fe: FetchError) -> Self {
Msg::Error {
emsg: fe.as_string(),
kopid: None,
}
}
}
// User Profile UI
pub struct ProfileApp {
state: ProfileAppState,
token: Option<String>,
user: Option<WhoamiResponse>,
}
pub struct ProfileApp {}
impl Component for ProfileApp {
type Message = Msg;
type Message = ();
type Properties = ViewProps;
fn create(ctx: &Context<Self>) -> Self {
fn create(_ctx: &Context<Self>) -> Self {
#[cfg(debug)]
console::debug!("views::profile::create");
// Submit a req to init the session.
// The uuid we want to submit against - hint, it's us.
let token = ctx.props().token.clone();
#[cfg(debug)]
console::debug!("token: ", &token);
ctx.link().send_future(async {
match Self::fetch_token_valid(token).await {
Ok(v) => v,
Err(v) => v.into(),
}
});
ProfileApp {
state: ProfileAppState::Loading,
token: None,
user: None,
}
ProfileApp {}
}
fn changed(&mut self, _ctx: &Context<Self>) -> bool {
#[cfg(debug)]
console::debug!("views::profile::changed");
false
fn changed(&mut self, ctx: &Context<Self>) -> bool {
console::debug!(format!(
"views::profile::changed current_user: {:?}",
ctx.props().current_user,
));
true
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
#[cfg(debug)]
console::debug!("views::profile::update");
match msg {
Msg::Error { emsg, kopid } => {
console::error!(format!(
"Failed to do something {:?} - kopid {:?}",
emsg, kopid
));
}
Msg::TokenInvalid => {
// TODO redirect off to login
let location = utils::window().location();
match location.replace("/") {
// No need to redraw, we are leaving.
Ok(_) => return false,
Err(e) => {
// Something went bang, opps.
console::error!(format!("{:?}", e).as_str());
// self.state = State::ErrInvalidRequest;
}
}
}
Msg::TokenValid(token) => {
// nothin' much
self.token = Some(token.clone());
#[cfg(debug)]
console::debug!(format!("Token is valid! ({})", token));
ctx.link().send_future(async {
match Self::fetch_user_data(token).await {
Ok(v) => v,
Err(v) => v.into(),
}
});
}
Msg::ProfileInfoRecieved(data) => {
#[cfg(debug)]
console::debug!(format!("ProfileInfoRecieved({:?})", data));
self.state = ProfileAppState::Loaded;
self.user = Some(data);
}
}
fn update(&mut self, ctx: &Context<Self>, _msg: Self::Message) -> bool {
console::debug!(format!(
"views::profile::update current_user: {:?}",
ctx.props().current_user,
));
true
}
@ -134,25 +39,17 @@ impl Component for ProfileApp {
}
/// UI view for the user profile
fn view(&self, _ctx: &Context<Self>) -> Html {
#[cfg(debug)]
console::debug!(format!(
"views::profile::starting view state: {:?}",
&self.state
));
let pagecontent = match self.state {
ProfileAppState::Loading => {
fn view(&self, ctx: &Context<Self>) -> Html {
let pagecontent = match &ctx.props().current_user {
None => {
html! {
<h2>
{"Loading user info..."}
</h2>
}
}
ProfileAppState::Loaded => {
Some(userinfo) => {
#[allow(clippy::unwrap_used)]
let userinfo = self.user.as_ref().unwrap();
let mail_primary = match userinfo.uat.mail_primary.as_ref() {
Some(email_address) => {
html! {
@ -238,77 +135,3 @@ impl Component for ProfileApp {
}
}
}
impl ProfileApp {
async fn fetch_token_valid(token: String) -> Result<Msg, FetchError> {
let mut opts = RequestInit::new();
opts.method("GET");
opts.mode(RequestMode::SameOrigin);
let request = Request::new_with_str_and_init("/v1/auth/valid", &opts)?;
request
.headers()
.set("content-type", "application/json")
.expect_throw("failed to set header");
request
.headers()
.set("authorization", format!("Bearer {}", token).as_str())
.expect_throw("failed to set header");
let window = crate::utils::window();
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
let resp: Response = resp_value.dyn_into().expect_throw("Invalid response type");
let status = resp.status();
if status == 200 {
Ok(Msg::TokenValid(token))
} else if status == 401 {
Ok(Msg::TokenInvalid)
} else {
let headers = resp.headers();
let kopid = headers.get("x-kanidm-opid").ok().flatten();
let text = JsFuture::from(resp.text()?).await?;
let emsg = text.as_string().unwrap_or_else(|| "".to_string());
Ok(Msg::Error { emsg, kopid })
}
}
async fn fetch_user_data(token: String) -> Result<Msg, FetchError> {
let mut opts = RequestInit::new();
opts.method("GET");
opts.mode(RequestMode::SameOrigin);
let request = Request::new_with_str_and_init("/v1/self", &opts)?;
request
.headers()
.set("content-type", "application/json")
.expect_throw("failed to set header");
request
.headers()
.set("authorization", format!("Bearer {}", token).as_str())
.expect_throw("failed to set header");
let window = utils::window();
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
let resp: Response = resp_value.dyn_into().expect_throw("Invalid response type");
let status = resp.status();
let headers = resp.headers();
let kopid = headers.get("x-kanidm-opid").ok().flatten();
if status == 200 {
let jsval = JsFuture::from(resp.json()?).await?;
let whoamiresponse: WhoamiResponse = jsval
.into_serde()
.map_err(|e| {
let e_msg = format!("serde error getting user data -> {:?}", e);
console::error!(e_msg.as_str());
})
.expect_throw("Invalid response type");
Ok(Msg::ProfileInfoRecieved(whoamiresponse))
} else {
let text = JsFuture::from(resp.text()?).await?;
let emsg = text.as_string().unwrap_or_else(|| "".to_string());
Ok(Msg::Error { emsg, kopid })
}
}
}

View file

@ -2,6 +2,7 @@ use crate::error::*;
use crate::models;
use crate::utils;
use crate::components::change_unix_password::ChangeUnixPassword;
use crate::manager::Route;
use crate::views::{ViewProps, ViewRoute};
@ -65,7 +66,7 @@ impl Component for SecurityApp {
fn changed(&mut self, _ctx: &Context<Self>) -> bool {
#[cfg(debug)]
console::debug!("views::security::changed");
false
true
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
@ -127,7 +128,7 @@ impl Component for SecurityApp {
State::Waiting => false,
};
let error = match &self.state {
let flash = match &self.state {
State::Error { emsg, kopid } => {
let message = match kopid {
Some(k) => format!("An error occured - {} - {}", emsg, k),
@ -136,19 +137,21 @@ impl Component for SecurityApp {
html! {
<div class="alert alert-danger alert-dismissible fade show" role="alert">
{ message }
<button type="button" class="btn btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
<button type="button" class="btn btn-close" data-dismiss="alert" aria-label="Close"></button>
</div>
}
}
_ => html! { <></> },
};
let current_user = ctx.props().current_user.clone();
html! {
<>
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h2>{ "Security" }</h2>
</div>
{ error }
{ flash }
<div>
<p>
<button type="button" class="btn btn-primary"
@ -164,6 +167,16 @@ impl Component for SecurityApp {
</button>
</p>
</div>
<hr/>
if let Some(user) = current_user {
if user.youare.attrs.get("class").map(|x| x.contains(&String::from("posixaccount"))).unwrap_or(true) {
<div>
<p>
<ChangeUnixPassword token={ctx.props().token.clone()}></ChangeUnixPassword>
</p>
</div>
}
}
</>
}
}
@ -175,7 +188,7 @@ impl SecurityApp {
opts.method("GET");
opts.mode(RequestMode::SameOrigin);
let uri = format!("/v1/account/{}/_credential/_update", id);
let uri = format!("/v1/person/{}/_credential/_update", id);
let request = Request::new_with_str_and_init(uri.as_str(), &opts)?;