mirror of
https://github.com/kanidm/kanidm.git
synced 2025-05-22 00:43:54 +02:00
* Refactor: move the object graph ui to admin web ui * Add dynamic js loading support Load viz.js dynamically * Add some js docs * chore: cleanup imports * chore: remove unused clipboard feature chore: remove unused mermaid.sh * Messing with the profile.release settings and reverting the changes I tried has now made the build much smaller yay :D * Refactor: user raw search requests Assert service-accounts properly * refactor: new v1 proto structure * Add self to CONTRIBUTORS.md --------- Co-authored-by: James Hodgkinson <james@terminaloutcomes.com>
863 lines
32 KiB
Rust
863 lines
32 KiB
Rust
use gloo::console;
|
|
use kanidm_proto::internal::{
|
|
CUCredState, CUExtPortal, CUIntentToken, CURegWarning, CUSessionToken, CUStatus,
|
|
CredentialDetail, CredentialDetailType, PasskeyDetail,
|
|
};
|
|
|
|
use kanidmd_web_ui_shared::constants::URL_USER_HOME;
|
|
use kanidmd_web_ui_shared::models::{get_cred_update_session, pop_return_location};
|
|
use kanidmd_web_ui_shared::utils::{autofocus, do_footer};
|
|
use kanidmd_web_ui_shared::{add_body_form_classes, logo_img, remove_body_form_classes};
|
|
use serde::Serialize;
|
|
use uuid::Uuid;
|
|
use wasm_bindgen::UnwrapThrowExt;
|
|
use yew::prelude::*;
|
|
use yew_router::prelude::*;
|
|
|
|
use super::delete::DeleteApp;
|
|
use super::passkey::PasskeyModalApp;
|
|
use super::passkeyremove::PasskeyRemoveModalApp;
|
|
use super::pwmodal::PwModalApp;
|
|
use super::totpmodal::TotpModalApp;
|
|
use super::totpremove::TotpRemoveComp;
|
|
use kanidmd_web_ui_shared::ui::error_page;
|
|
use kanidmd_web_ui_shared::{do_request, error::FetchError, utils, RequestMethod};
|
|
|
|
// use std::rc::Rc;
|
|
|
|
#[derive(Debug, Clone)]
|
|
#[allow(clippy::large_enum_variant)]
|
|
pub enum EventBusMsg {
|
|
UpdateStatus { status: CUStatus },
|
|
Error { emsg: String, kopid: Option<String> },
|
|
}
|
|
|
|
#[derive(PartialEq, Properties)]
|
|
pub struct ModalProps {
|
|
pub token: CUSessionToken,
|
|
pub cb: Callback<EventBusMsg>,
|
|
}
|
|
|
|
#[derive(PartialEq, Properties)]
|
|
pub struct TotpRemoveProps {
|
|
pub token: CUSessionToken,
|
|
pub label: String,
|
|
pub cb: Callback<EventBusMsg>,
|
|
}
|
|
|
|
#[derive(PartialEq)]
|
|
pub enum PasskeyClass {
|
|
Any,
|
|
Attested,
|
|
}
|
|
|
|
#[derive(PartialEq, Properties)]
|
|
pub struct PasskeyModalProps {
|
|
pub token: CUSessionToken,
|
|
pub class: PasskeyClass,
|
|
pub allowed_devices: Option<Vec<String>>,
|
|
pub cb: Callback<EventBusMsg>,
|
|
}
|
|
|
|
#[derive(PartialEq, Properties)]
|
|
pub struct PasskeyRemoveModalProps {
|
|
pub token: CUSessionToken,
|
|
pub class: PasskeyClass,
|
|
pub tag: String,
|
|
pub uuid: Uuid,
|
|
pub cb: Callback<EventBusMsg>,
|
|
}
|
|
|
|
pub enum Msg {
|
|
TokenSubmit,
|
|
BeginSession {
|
|
token: CUSessionToken,
|
|
status: CUStatus,
|
|
},
|
|
UpdateSession {
|
|
status: CUStatus,
|
|
},
|
|
Cancel,
|
|
Commit,
|
|
Success,
|
|
Error {
|
|
emsg: String,
|
|
kopid: Option<String>,
|
|
},
|
|
// TODO: use this? :)
|
|
#[allow(dead_code)]
|
|
Ignore,
|
|
}
|
|
|
|
impl From<FetchError> for Msg {
|
|
fn from(fe: FetchError) -> Self {
|
|
Msg::Error {
|
|
emsg: fe.as_string(),
|
|
kopid: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::large_enum_variant)]
|
|
//Page state
|
|
enum State {
|
|
TokenInput,
|
|
WaitingForStatus,
|
|
Main {
|
|
token: CUSessionToken,
|
|
status: CUStatus,
|
|
},
|
|
WaitingForCommit,
|
|
#[allow(clippy::large_enum_variant)]
|
|
Error {
|
|
emsg: String,
|
|
kopid: Option<String>,
|
|
},
|
|
}
|
|
|
|
pub struct CredentialResetApp {
|
|
state: State,
|
|
cb: Callback<EventBusMsg>,
|
|
}
|
|
|
|
impl Component for CredentialResetApp {
|
|
type Message = Msg;
|
|
type Properties = ();
|
|
|
|
fn create(ctx: &Context<Self>) -> Self {
|
|
#[cfg(debug_assertions)]
|
|
console::debug!("credential::reset::create");
|
|
|
|
// On a page refresh/reload, should we restart a session that *may* have existed?
|
|
// This could be achieved with local storage
|
|
|
|
// Where did we come from?
|
|
|
|
add_body_form_classes!();
|
|
|
|
// Can we pre-load in a session token? This occurs when we are sent a
|
|
// credential reset from the views UI.
|
|
|
|
/* Were we given a token for the reset? */
|
|
|
|
let location = ctx
|
|
.link()
|
|
.location()
|
|
.expect_throw("Can't access current location");
|
|
|
|
// TODO: the error here ... isn't always an error, when a user comes from the dashboard they don't set a cred token in the URL, probably should handle this with a *slightly* nicer error
|
|
let query: Option<CUIntentToken> = location
|
|
.query()
|
|
.map_err(|e| {
|
|
let e_msg = format!("error decoding URL parameters -> {:?}", e);
|
|
console::error!(e_msg.as_str());
|
|
})
|
|
.ok();
|
|
|
|
let m_session = get_cred_update_session();
|
|
|
|
let state = match (query, m_session) {
|
|
(Some(cu_intent), None) => {
|
|
// Go straight to go! Collect 200!
|
|
ctx.link().send_future(async {
|
|
match Self::exchange_intent_token(cu_intent.token).await {
|
|
Ok(v) => v,
|
|
Err(v) => v.into(),
|
|
}
|
|
});
|
|
State::WaitingForStatus
|
|
}
|
|
(None, Some((token, status))) => State::Main { token, status },
|
|
(None, None) => State::TokenInput,
|
|
(Some(_), Some(_)) => State::Error {
|
|
emsg: "Invalid State - Reset link and memory session both are available!"
|
|
.to_string(),
|
|
kopid: None,
|
|
},
|
|
};
|
|
|
|
let cb = Callback::from({
|
|
let link = ctx.link().clone();
|
|
move |emsg| {
|
|
let msg = match emsg {
|
|
EventBusMsg::UpdateStatus { status } => Msg::UpdateSession { status },
|
|
EventBusMsg::Error { emsg, kopid } => Msg::Error { emsg, kopid },
|
|
};
|
|
link.send_message(msg);
|
|
}
|
|
});
|
|
|
|
CredentialResetApp { state, cb }
|
|
}
|
|
|
|
fn changed(&mut self, _ctx: &Context<Self>, _props: &Self::Properties) -> bool {
|
|
#[cfg(debug_assertions)]
|
|
console::debug!("credential::reset::change");
|
|
false
|
|
}
|
|
|
|
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
|
#[cfg(debug_assertions)]
|
|
console::debug!("credential::reset::update");
|
|
let next_state = match (msg, &self.state) {
|
|
(Msg::Ignore, _) => None,
|
|
(Msg::TokenSubmit, State::TokenInput) => {
|
|
#[allow(clippy::expect_used)]
|
|
let token = utils::get_value_from_element_id("token")
|
|
.expect("Unable to find an input with id=token");
|
|
|
|
ctx.link().send_future(async {
|
|
match Self::exchange_intent_token(token).await {
|
|
Ok(v) => v,
|
|
Err(v) => v.into(),
|
|
}
|
|
});
|
|
|
|
Some(State::WaitingForStatus)
|
|
}
|
|
(Msg::BeginSession { token, status }, State::WaitingForStatus) => {
|
|
#[cfg(debug_assertions)]
|
|
console::debug!(format!("begin session {:?}", status).as_str());
|
|
Some(State::Main { token, status })
|
|
}
|
|
(Msg::UpdateSession { status }, State::Main { token, status: _ }) => {
|
|
#[cfg(debug_assertions)]
|
|
console::debug!(format!("{:?}", status).as_str());
|
|
Some(State::Main {
|
|
token: token.clone(),
|
|
status,
|
|
})
|
|
}
|
|
(Msg::Commit, State::Main { token, status }) => {
|
|
console::debug!(format!("{:?}", status).as_str());
|
|
let token_c = token.clone();
|
|
|
|
ctx.link().send_future(async {
|
|
match Self::commit_session(token_c).await {
|
|
Ok(v) => v,
|
|
Err(v) => v.into(),
|
|
}
|
|
});
|
|
|
|
Some(State::WaitingForCommit)
|
|
}
|
|
(Msg::Cancel, State::Main { token, status: _ }) => {
|
|
#[cfg(debug_assertions)]
|
|
console::debug!("msg::cancel");
|
|
let token_c = token.clone();
|
|
|
|
ctx.link().send_future(async {
|
|
match Self::cancel_session(token_c).await {
|
|
Ok(v) => v,
|
|
Err(v) => v.into(),
|
|
}
|
|
});
|
|
|
|
Some(State::WaitingForCommit)
|
|
}
|
|
(Msg::Success, State::WaitingForCommit) => {
|
|
let loc = pop_return_location();
|
|
#[cfg(debug_assertions)]
|
|
console::debug!(["Successful, redirecting to -> ", &loc].concat());
|
|
let window = gloo_utils::window();
|
|
window.location().set_href(&loc.to_string()).unwrap_throw();
|
|
None
|
|
}
|
|
(Msg::Error { emsg, kopid }, _) => Some(State::Error { emsg, kopid }),
|
|
(_, _) => {
|
|
console::error!("CredentialResetApp state match fail on update!");
|
|
None
|
|
}
|
|
};
|
|
|
|
if let Some(mut next_state) = next_state {
|
|
std::mem::swap(&mut self.state, &mut next_state);
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
|
|
#[cfg(debug_assertions)]
|
|
console::debug!("credential::reset::rendered");
|
|
// because sometimes bootstrap doesn't catch it, which is annoying.
|
|
autofocus("token");
|
|
}
|
|
|
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
|
#[cfg(debug_assertions)]
|
|
console::debug!("credential::reset::view");
|
|
match &self.state {
|
|
State::TokenInput => self.view_token_input(ctx),
|
|
State::WaitingForStatus | State::WaitingForCommit => self.view_waiting(ctx),
|
|
State::Main { token, status } => self.view_main(ctx, token, status),
|
|
State::Error { emsg, kopid } => self.view_error(ctx, emsg, kopid.as_deref()),
|
|
}
|
|
}
|
|
|
|
fn destroy(&mut self, _ctx: &Context<Self>) {
|
|
#[cfg(debug_assertions)]
|
|
console::debug!("credential::reset::destroy");
|
|
remove_body_form_classes!();
|
|
}
|
|
}
|
|
|
|
impl CredentialResetApp {
|
|
fn view_token_input(&self, ctx: &Context<Self>) -> Html {
|
|
html! {
|
|
<main class="flex-shrink-0 form-signin">
|
|
<center>
|
|
{logo_img()}
|
|
<h2>{ "Credential Reset" } </h2>
|
|
// TODO: replace this with a call to domain info
|
|
// <h3>{ "idm.example.com" } </h3>
|
|
</center>
|
|
<form
|
|
onsubmit={ ctx.link().callback(|e: SubmitEvent| {
|
|
console::debug!("credential::reset::view_token_input -> TokenInput - prevent_default()");
|
|
e.prevent_default();
|
|
|
|
Msg::TokenSubmit
|
|
} ) }
|
|
action="javascript:void(0);">
|
|
<p class="text-center">
|
|
<label for="token" class="form-label">
|
|
{"Enter your credential reset token."}
|
|
</label>
|
|
<input
|
|
id="token"
|
|
name="token"
|
|
autofocus=true
|
|
type="text"
|
|
class="form-control"
|
|
value=""
|
|
/>
|
|
</p>
|
|
<p class="text-center">
|
|
<button type="submit" class="btn btn-primary">{" Submit "}</button><br />
|
|
</p>
|
|
</form>
|
|
<p class="text-center">
|
|
<a href={URL_USER_HOME}><button href={URL_USER_HOME} class="btn btn-secondary" aria-label="Return home">{"Return to the home page"}</button></a>
|
|
</p>
|
|
|
|
</main>
|
|
}
|
|
}
|
|
|
|
fn view_waiting(&self, _ctx: &Context<Self>) -> Html {
|
|
html! {
|
|
<main class="text-center form-signin h-100">
|
|
<div class="vert-center">
|
|
<div class="spinner-border text-dark" role="status">
|
|
<span class="visually-hidden">{ "Loading..." }</span>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
}
|
|
}
|
|
|
|
fn view_main(&self, ctx: &Context<Self>, token: &CUSessionToken, status: &CUStatus) -> Html {
|
|
remove_body_form_classes!();
|
|
|
|
let CUStatus {
|
|
spn,
|
|
displayname,
|
|
ext_cred_portal,
|
|
mfaregstate: _,
|
|
can_commit,
|
|
warnings,
|
|
primary,
|
|
primary_state,
|
|
passkeys,
|
|
passkeys_state,
|
|
attested_passkeys,
|
|
attested_passkeys_state,
|
|
attested_passkeys_allowed_devices,
|
|
} = status;
|
|
|
|
let (username, domain) = spn.split_once('@').unwrap_or(("", &spn));
|
|
let names = format!("{} ({})", displayname, username);
|
|
let cb = self.cb.clone();
|
|
|
|
let ext_cred_portal_html = match ext_cred_portal {
|
|
CUExtPortal::None => html! { <></> },
|
|
CUExtPortal::Hidden => html! {
|
|
<>
|
|
<hr class="my-4" />
|
|
<p>{ "This account is externally managed. Some features may not be available." }</p>
|
|
</>
|
|
},
|
|
CUExtPortal::Some(url) => {
|
|
let url_str = url.as_str().to_string();
|
|
html! {
|
|
<>
|
|
<hr class="my-4" />
|
|
<p>{ "This account is externally managed. Some features may not be available." }</p>
|
|
<a href={ url_str } >{ "Visit the external account portal" }</a>
|
|
</>
|
|
}
|
|
}
|
|
};
|
|
|
|
let pw_html = self.view_primary(token, primary, *primary_state);
|
|
let passkey_html = self.view_passkeys(token, passkeys, *passkeys_state);
|
|
let attested_passkey_html = self.view_attested_passkeys(
|
|
token,
|
|
attested_passkeys,
|
|
*attested_passkeys_state,
|
|
attested_passkeys_allowed_devices.as_slice(),
|
|
);
|
|
|
|
let warnings_html = if warnings.is_empty() {
|
|
html! { <></> }
|
|
} else {
|
|
html! {
|
|
<>
|
|
<hr class="my-4" />
|
|
|
|
{ for warnings.iter()
|
|
.map(|warning|
|
|
match warning {
|
|
CURegWarning::MfaRequired => html! {
|
|
<div class="alert alert-warning" role="alert">
|
|
<p>{ "Multi-Factor Authentication is required for your account. Either add TOTP or remove your password in favour of passkeys to submit." }</p>
|
|
</div>
|
|
},
|
|
CURegWarning::PasskeyRequired => html! {
|
|
<div class="alert alert-warning" role="alert">
|
|
<p>{ "Passkeys are required for your account." }</p>
|
|
</div>
|
|
},
|
|
CURegWarning::AttestedPasskeyRequired => html! {
|
|
<div class="alert alert-warning" role="alert">
|
|
<p>{ "Attested Passkeys are required for your account." }</p>
|
|
</div>
|
|
},
|
|
CURegWarning::AttestedResidentKeyRequired => html! {
|
|
<div class="alert alert-warning" role="alert">
|
|
<p>{ "Attested Resident Keys are required for your account." }</p>
|
|
</div>
|
|
},
|
|
CURegWarning::WebauthnAttestationUnsatisfiable => html! {
|
|
<div class="alert alert-danger" role="alert">
|
|
<p>{ "A webauthn attestation policy conflict has occurred and you will not be able to save your credentials" }</p>
|
|
<p>{ "Contact support IMMEDIATELY." }</p>
|
|
</div>
|
|
},
|
|
CURegWarning::Unsatisfiable => html! {
|
|
<div class="alert alert-danger" role="alert">
|
|
<p>{ "An account policy conflict has occurred and you will not be able to save your credentials" }</p>
|
|
<p>{ "Contact support IMMEDIATELY." }</p>
|
|
</div>
|
|
},
|
|
}
|
|
)
|
|
}
|
|
</>
|
|
}
|
|
};
|
|
|
|
html! {
|
|
<>
|
|
<div class="d-flex align-items-start form-cred-reset-body">
|
|
<main class="w-100">
|
|
<div class="py-3 text-center">
|
|
<h3>{ "Updating Credentials" }</h3>
|
|
<p>{ names }</p>
|
|
<p>{ domain }</p>
|
|
</div>
|
|
|
|
<div class="row g-3">
|
|
<form class="needs-validation" novalidate=true>
|
|
|
|
{ ext_cred_portal_html }
|
|
|
|
{ warnings_html }
|
|
|
|
{ attested_passkey_html }
|
|
|
|
{ passkey_html }
|
|
|
|
{ pw_html }
|
|
|
|
<hr class="my-4" />
|
|
|
|
<button class="w-50 btn btn-danger btn-lg"
|
|
disabled=false
|
|
onclick={
|
|
ctx.link()
|
|
.callback(move |_| {
|
|
Msg::Cancel
|
|
})
|
|
}
|
|
>{ "Cancel" }</button>
|
|
<button
|
|
class="w-50 btn btn-success btn-lg"
|
|
disabled={ !can_commit }
|
|
onclick={
|
|
ctx.link()
|
|
.callback(move |_| {
|
|
Msg::Commit
|
|
})
|
|
}
|
|
type="submit"
|
|
>{ "Submit Changes" }</button>
|
|
</form>
|
|
</div>
|
|
</main>
|
|
|
|
<DeleteApp token= { token.clone() } cb={ cb.clone() }/>
|
|
|
|
</div>
|
|
{ do_footer() }
|
|
</>
|
|
}
|
|
}
|
|
|
|
fn view_primary(
|
|
&self,
|
|
token: &CUSessionToken,
|
|
primary: &Option<CredentialDetail>,
|
|
primary_state: CUCredState,
|
|
) -> Html {
|
|
let cb = self.cb.clone();
|
|
|
|
// match on primary, get type_.
|
|
let alt_auth_method_inner = if matches!(primary_state, CUCredState::Modifiable) {
|
|
match primary {
|
|
Some(CredentialDetail {
|
|
uuid: _,
|
|
type_: CredentialDetailType::Password,
|
|
}) => {
|
|
html! {
|
|
<>
|
|
<h6> <b>{ "Password" }</b> </h6>
|
|
<p>
|
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#staticPassword">
|
|
{ "Change Password" }
|
|
</button>
|
|
</p>
|
|
<h6> <b>{ "Time-based One Time Password (TOTP)" }</b> </h6>
|
|
<p>{ "TOTPs are 6 digit codes generated on-demand as a second authentication factor."}</p>
|
|
<p>
|
|
<TotpModalApp token={ token.clone() } cb={ cb.clone() }/>
|
|
</p>
|
|
<br/>
|
|
<p>
|
|
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#staticDeletePrimaryCred">
|
|
{ "Delete Alternative Credentials" }
|
|
</button>
|
|
</p>
|
|
</>
|
|
}
|
|
}
|
|
Some(CredentialDetail {
|
|
uuid: _,
|
|
type_:
|
|
CredentialDetailType::PasswordMfa(
|
|
// Used for what TOTP the user has.
|
|
totp_set,
|
|
// Being deprecated.
|
|
_security_key_labels,
|
|
// Need to wire in backup codes.
|
|
_backup_codes_remaining,
|
|
),
|
|
}) => {
|
|
html! {
|
|
<>
|
|
<h6> <b>{ "Password" }</b> </h6>
|
|
<p>
|
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#staticPassword">
|
|
{ "Change Password" }
|
|
</button>
|
|
</p>
|
|
<br/>
|
|
<h6> <b>{ "Time-based One Time Password (TOTP)" }</b></h6>
|
|
<p>{ "TOTPs are 6 digit codes generated on-demand as a second authentication factor."}</p>
|
|
<>
|
|
{ for totp_set.iter()
|
|
.map(|detail| html! { <TotpRemoveComp token={ token.clone() } label={ detail.clone() } cb={ cb.clone() } /> })
|
|
}
|
|
</>
|
|
|
|
<p>
|
|
<TotpModalApp token={ token.clone() } cb={ cb.clone() }/>
|
|
</p>
|
|
<br/>
|
|
<br/>
|
|
<p>
|
|
<button type="button" class="btn btn-warning" data-bs-toggle="modal" data-bs-target="#staticDeletePrimaryCred">
|
|
{ "Delete Alternative Credentials" }
|
|
</button>
|
|
</p>
|
|
|
|
</>
|
|
}
|
|
}
|
|
Some(CredentialDetail {
|
|
uuid: _,
|
|
type_: CredentialDetailType::GeneratedPassword,
|
|
}) => {
|
|
html! {
|
|
<>
|
|
<h6> <b>{ "Password" }</b> </h6>
|
|
<p>{ "In order to set up alternative authentication methods, you must delete the generated password." }</p>
|
|
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#staticDeletePrimaryCred">
|
|
{ "Delete Generated Password" }
|
|
</button>
|
|
</>
|
|
}
|
|
}
|
|
Some(CredentialDetail {
|
|
uuid: _,
|
|
type_: CredentialDetailType::Passkey(_),
|
|
}) => {
|
|
html! {
|
|
<>
|
|
<p>{ "Webauthn Only - Will migrate to passkeys in a future update" }</p>
|
|
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#staticDeletePrimaryCred">
|
|
{ "Delete Alternative Credentials" }
|
|
</button>
|
|
</>
|
|
}
|
|
}
|
|
None => {
|
|
html! {
|
|
<button type="button" class="btn btn-warning" data-bs-toggle="modal" data-bs-target="#staticPassword">
|
|
{ "Add Password" }
|
|
</button>
|
|
}
|
|
}
|
|
}
|
|
} else if matches!(primary_state, CUCredState::DeleteOnly) {
|
|
html! {
|
|
<p>
|
|
<button type="button" class="btn btn-warning" data-bs-toggle="modal" data-bs-target="#staticDeletePrimaryCred">
|
|
{ "Delete Legacy Credentials" }
|
|
</button>
|
|
</p>
|
|
}
|
|
} else {
|
|
html! {<></>}
|
|
};
|
|
|
|
let alt_auth_method_warning = match primary_state {
|
|
CUCredState::Modifiable => {
|
|
html! {
|
|
<>
|
|
<p>{ "If possible, passkeys should be used instead, as they are phishing and exploit resistant." }</p>
|
|
</>
|
|
}
|
|
}
|
|
CUCredState::DeleteOnly => {
|
|
html! {
|
|
<>
|
|
<p>{ "If possible, passkeys should be used instead, as they are phishing and exploit resistant." }</p>
|
|
<p>{ "Account policy prevents you modifying these credentials, but you may remove them." }</p>
|
|
</>
|
|
}
|
|
}
|
|
CUCredState::AccessDeny => {
|
|
html! {
|
|
<>
|
|
<p>{ "You do not have access to modify these credentials." }</p>
|
|
</>
|
|
}
|
|
}
|
|
CUCredState::PolicyDeny => {
|
|
html! {
|
|
<>
|
|
<p>{ "Account policy prevents you from setting these credentials" }</p>
|
|
</>
|
|
}
|
|
}
|
|
};
|
|
|
|
html! {
|
|
<>
|
|
<hr class="my-4" />
|
|
<h4>{"Alternative Authentication Methods" }</h4>
|
|
{ alt_auth_method_warning }
|
|
{ alt_auth_method_inner }
|
|
|
|
<PwModalApp token={ token.clone() } cb={ cb } />
|
|
</>
|
|
}
|
|
}
|
|
|
|
fn view_passkeys(
|
|
&self,
|
|
token: &CUSessionToken,
|
|
passkeys: &[PasskeyDetail],
|
|
passkeys_state: CUCredState,
|
|
) -> Html {
|
|
let cb = self.cb.clone();
|
|
|
|
match passkeys_state {
|
|
CUCredState::DeleteOnly | CUCredState::Modifiable => {
|
|
html! {
|
|
<>
|
|
<hr class="my-4" />
|
|
<h4>{"Passkeys"}</h4>
|
|
|
|
<p>{ "Easy to use digital credentials with self-contained multi-factor authentication designed to replace passwords." }</p>
|
|
<p>
|
|
<a target="_blank" href="https://support.microsoft.com/en-us/windows/passkeys-in-windows-301c8944-5ea2-452b-9886-97e4d2ef4422">{ "Windows" }</a>
|
|
{ ", " }
|
|
<a target="_blank" href="https://support.apple.com/guide/mac-help/create-a-passkey-mchl4af65d1a/mac">{ "MacOS" }</a>
|
|
{ ", " }
|
|
<a target="_blank" href="https://support.google.com/android/answer/14124480?hl=en">{ "Android" }</a>
|
|
{ ", and " }
|
|
<a target="_blank" href="https://support.apple.com/guide/iphone/use-passkeys-to-sign-in-to-apps-and-websites-iphf538ea8d0/ios">{ "iOS" }</a>
|
|
{ " have built-in support for passkeys."}
|
|
</p>
|
|
|
|
{ for passkeys.iter()
|
|
.map(|detail|
|
|
PasskeyRemoveModalApp::render_button(&detail.tag, detail.uuid)
|
|
)
|
|
}
|
|
{ for passkeys.iter()
|
|
.map(|detail|
|
|
html! { <PasskeyRemoveModalApp token={ token.clone() } tag={ detail.tag.clone() } uuid={ detail.uuid } cb={ cb.clone() } class={ PasskeyClass::Any } /> }
|
|
)
|
|
}
|
|
|
|
{ if passkeys_state == CUCredState::Modifiable {
|
|
html! { <PasskeyModalApp token={ token.clone() } cb={ cb.clone() } class={ PasskeyClass::Any } /> }
|
|
} else {
|
|
html! { <></> }
|
|
}}
|
|
</>
|
|
}
|
|
}
|
|
CUCredState::AccessDeny => {
|
|
html! { <></> }
|
|
}
|
|
CUCredState::PolicyDeny => {
|
|
html! { <></> }
|
|
}
|
|
}
|
|
}
|
|
|
|
fn view_attested_passkeys(
|
|
&self,
|
|
token: &CUSessionToken,
|
|
attested_passkeys: &[PasskeyDetail],
|
|
attested_passkeys_state: CUCredState,
|
|
attested_passkeys_allowed_devices: &[String],
|
|
) -> Html {
|
|
let cb = self.cb.clone();
|
|
|
|
match attested_passkeys_state {
|
|
CUCredState::Modifiable => {
|
|
html! {
|
|
<>
|
|
<hr class="my-4" />
|
|
<h4>{"Attested Passkeys"}</h4>
|
|
{ for attested_passkeys.iter()
|
|
.map(|detail|
|
|
PasskeyRemoveModalApp::render_button(&detail.tag, detail.uuid)
|
|
)
|
|
}
|
|
{ for attested_passkeys.iter()
|
|
.map(|detail|
|
|
html! { <PasskeyRemoveModalApp token={ token.clone() } tag={ detail.tag.clone() } uuid={ detail.uuid } cb={ cb.clone() } class={ PasskeyClass::Attested } /> }
|
|
)
|
|
}
|
|
|
|
<PasskeyModalApp token={ token.clone() } cb={ cb } class={ PasskeyClass::Attested } allowed_devices={ Some(attested_passkeys_allowed_devices.to_vec()) } />
|
|
</>
|
|
}
|
|
}
|
|
CUCredState::DeleteOnly => {
|
|
if attested_passkeys.is_empty() {
|
|
html! { <></> }
|
|
} else {
|
|
html! {
|
|
<>
|
|
<hr class="my-4" />
|
|
<h4>{"Attested Passkeys"}</h4>
|
|
|
|
{ for attested_passkeys.iter()
|
|
.map(|detail|
|
|
PasskeyRemoveModalApp::render_button(&detail.tag, detail.uuid)
|
|
)
|
|
}
|
|
{ for attested_passkeys.iter()
|
|
.map(|detail|
|
|
html! { <PasskeyRemoveModalApp token={ token.clone() } tag={ detail.tag.clone() } uuid={ detail.uuid } cb={ cb.clone() } class={ PasskeyClass::Attested } /> }
|
|
)
|
|
}
|
|
|
|
</>
|
|
}
|
|
}
|
|
}
|
|
CUCredState::AccessDeny | CUCredState::PolicyDeny => {
|
|
// Don't display anything.
|
|
html! { <></> }
|
|
}
|
|
}
|
|
}
|
|
|
|
fn view_error(&self, _ctx: &Context<Self>, msg: &str, kopid: Option<&str>) -> Html {
|
|
html! {
|
|
<main class="form-signin">
|
|
{ error_page(msg, kopid) }
|
|
</main>
|
|
}
|
|
}
|
|
|
|
async fn exchange_intent_token(token: String) -> Result<Msg, FetchError> {
|
|
let request = CUIntentToken { token };
|
|
let req_jsvalue = request
|
|
.serialize(&serde_wasm_bindgen::Serializer::json_compatible())
|
|
.expect("Failed to serialise request");
|
|
let req_jsvalue = js_sys::JSON::stringify(&req_jsvalue).expect_throw("failed to stringify");
|
|
|
|
let (kopid, status, value, _) = do_request(
|
|
"/v1/credential/_exchange_intent",
|
|
RequestMethod::POST,
|
|
Some(req_jsvalue),
|
|
)
|
|
.await?;
|
|
|
|
if status == 200 {
|
|
let (token, status): (CUSessionToken, CUStatus) =
|
|
serde_wasm_bindgen::from_value(value).expect_throw("Invalid response type");
|
|
Ok(Msg::BeginSession { token, status })
|
|
} else {
|
|
let emsg = value.as_string().unwrap_or_default();
|
|
Ok(Msg::Error { emsg, kopid })
|
|
}
|
|
}
|
|
|
|
async fn end_session(token: CUSessionToken, url: &str) -> Result<Msg, FetchError> {
|
|
let req_jsvalue = token
|
|
.serialize(&serde_wasm_bindgen::Serializer::json_compatible())
|
|
.expect("Failed to serialise request");
|
|
let req_jsvalue = js_sys::JSON::stringify(&req_jsvalue).expect_throw("failed to stringify");
|
|
|
|
let (kopid, status, value, _) =
|
|
do_request(url, RequestMethod::POST, Some(req_jsvalue)).await?;
|
|
|
|
if status == 200 {
|
|
Ok(Msg::Success)
|
|
} else {
|
|
let emsg = value.as_string().unwrap_or_default();
|
|
Ok(Msg::Error { emsg, kopid })
|
|
}
|
|
}
|
|
|
|
async fn cancel_session(token: CUSessionToken) -> Result<Msg, FetchError> {
|
|
Self::end_session(token, "/v1/credential/_cancel").await
|
|
}
|
|
|
|
async fn commit_session(token: CUSessionToken) -> Result<Msg, FetchError> {
|
|
Self::end_session(token, "/v1/credential/_commit").await
|
|
}
|
|
}
|