kanidm/kanidmd_web_ui/src/login.rs
2022-09-25 11:21:30 +10:00

960 lines
38 KiB
Rust

// use anyhow::Error;
use gloo::console;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::{spawn_local, JsFuture};
use web_sys::{CredentialRequestOptions, Request, RequestInit, RequestMode, Response};
use yew::prelude::*;
use yew::virtual_dom::VNode;
use yew_router::prelude::*;
use crate::constants::{CLASS_BUTTON_DARK, CLASS_DIV_LOGIN_BUTTON, CLASS_DIV_LOGIN_FIELD};
use crate::error::FetchError;
use crate::models;
use crate::utils;
use kanidm_proto::v1::{
AuthAllowed, AuthCredential, AuthMech, AuthRequest, AuthResponse, AuthState, AuthStep,
};
use kanidm_proto::webauthn::PublicKeyCredential;
pub struct LoginApp {
inputvalue: String,
session_id: String,
state: LoginState,
}
#[derive(PartialEq)]
enum TotpState {
Enabled,
Disabled,
Invalid,
}
enum LoginState {
Init(bool),
// Select between different cred types, either password (and MFA) or Passkey
Select(Vec<AuthMech>),
// The choices of authentication mechanism.
Continue(Vec<AuthAllowed>),
// The different methods
Password(bool),
BackupCode(bool),
Totp(TotpState),
Passkey(CredentialRequestOptions),
SecurityKey(CredentialRequestOptions),
// Error, state handling.
Error { emsg: String, kopid: Option<String> },
UnknownUser,
Denied(String),
Authenticated,
}
pub enum LoginAppMsg {
Input(String),
Restart,
Begin,
PasswordSubmit,
BackupCodeSubmit,
TotpSubmit,
PasskeySubmit(PublicKeyCredential),
SecurityKeySubmit(PublicKeyCredential),
Start(String, AuthResponse),
Next(AuthResponse),
Continue(usize),
Select(usize),
// DoNothing,
UnknownUser,
Error { emsg: String, kopid: Option<String> },
}
impl From<FetchError> for LoginAppMsg {
fn from(fe: FetchError) -> Self {
LoginAppMsg::Error {
emsg: fe.as_string(),
kopid: None,
}
}
}
impl LoginApp {
async fn auth_init(username: String) -> Result<LoginAppMsg, FetchError> {
let authreq = AuthRequest {
step: AuthStep::Init(username),
};
let authreq_jsvalue = serde_json::to_string(&authreq)
.map(|s| JsValue::from(&s))
.expect_throw("Failed to serialise authreq");
let mut opts = RequestInit::new();
opts.method("POST");
opts.mode(RequestMode::SameOrigin);
opts.body(Some(&authreq_jsvalue));
let request = Request::new_with_str_and_init("/v1/auth", &opts)?;
request
.headers()
.set("content-type", "application/json")
.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 - auth_init::Response");
let status = resp.status();
let headers = resp.headers();
if status == 200 {
let session_id = headers
.get("x-kanidm-auth-session-id")
.ok()
.flatten()
.unwrap_or_else(|| "".to_string());
let jsval = JsFuture::from(resp.json()?).await?;
let state: AuthResponse = serde_wasm_bindgen::from_value(jsval)
.expect_throw("Invalid response type - auth_init::AuthResponse");
Ok(LoginAppMsg::Start(session_id, state))
} else if status == 404 {
let kopid = headers.get("x-kanidm-opid").ok().flatten();
let text = JsFuture::from(resp.text()?).await?;
console::error!(format!(
"User not found: {:?}. Operation ID: {:?}",
text, kopid
));
Ok(LoginAppMsg::UnknownUser)
} else {
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(LoginAppMsg::Error { emsg, kopid })
}
}
async fn auth_step(
authreq: AuthRequest,
session_id: String,
) -> Result<LoginAppMsg, FetchError> {
let authreq_jsvalue = serde_json::to_string(&authreq)
.map(|s| JsValue::from(&s))
.expect_throw("Failed to serialise authreq");
let mut opts = RequestInit::new();
opts.method("POST");
opts.mode(RequestMode::SameOrigin);
opts.body(Some(&authreq_jsvalue));
let request = Request::new_with_str_and_init("/v1/auth", &opts)?;
request
.headers()
.set("content-type", "application/json")
.expect_throw("failed to set content-type header");
request
.headers()
.set("x-kanidm-auth-session-id", session_id.as_str())
.expect_throw("failed to set x-kanidm-auth-session-id 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 - auth_step::Response");
let status = resp.status();
let headers = resp.headers();
if status == 200 {
let jsval = JsFuture::from(resp.json()?).await?;
let state: AuthResponse = serde_wasm_bindgen::from_value(jsval)
.map_err(|e| {
console::error!(format!("auth_step::AuthResponse: {:?}", e));
e
})
.expect_throw("Invalid response type - auth_step::AuthResponse");
Ok(LoginAppMsg::Next(state))
} else {
let kopid = headers.get("x-kanidm-opid").ok().flatten();
let text = JsFuture::from(resp.text()?).await?;
let emsg = text.as_string()
.unwrap_or_else(|| "Unhandled error, please report this along with the operation ID below to your administrator. 😔".to_string());
Ok(LoginAppMsg::Error { emsg, kopid })
}
}
/// Renders the "Start again" button
fn button_start_again(&self, ctx: &Context<Self>) -> VNode {
html! {
<div class="col-md-auto text-center">
<button type="button" class={CLASS_BUTTON_DARK} onclick={ ctx.link().callback(|_| LoginAppMsg::Restart) } >{" Start Again "}</button>
</div>
}
}
fn render_auth_allowed(&self, ctx: &Context<Self>, idx: usize, allow: &AuthAllowed) -> Html {
html! {
<li class="text-center mb-2">
<button
type="button"
class={CLASS_BUTTON_DARK}
onclick={ ctx.link().callback(move |_| LoginAppMsg::Continue(idx)) }
>{ allow.to_string() }</button>
</li>
}
}
fn render_mech_select(&self, ctx: &Context<Self>, idx: usize, allow: &AuthMech) -> Html {
html! {
<li class="text-center mb-2">
<button
type="button"
class={CLASS_BUTTON_DARK}
onclick={ ctx.link().callback(move |_| LoginAppMsg::Select(idx)) }
>{ allow.to_string() }</button>
</li>
}
}
/// shows an error-alert in a bootstrap alert container
fn do_alert_error(
&self,
alert_title: &str,
alert_message: Option<&str>,
ctx: &Context<Self>,
) -> VNode {
html! {
<div class="container">
<div class="row justify-content-md-center">
<div class="alert alert-danger" role="alert">
<p><strong>{ alert_title }</strong></p>
if let Some(value) = alert_message {
<p>{ value }</p>
}
</div>
{ self.button_start_again(ctx) }
</div>
</div>
}
}
fn view_state(&self, ctx: &Context<Self>) -> Html {
let inputvalue = self.inputvalue.clone();
match &self.state {
LoginState::Init(enable) => {
html! {
<>
<div class="container">
<label for="username" class="form-label">{ "Username" }</label>
<form
onsubmit={ ctx.link().callback(|e: FocusEvent| {
#[cfg(debug)]
console::debug!("login::view_state -> Init - prevent_default()".to_string());
e.prevent_default();
LoginAppMsg::Begin
} ) }
>
<div class={CLASS_DIV_LOGIN_FIELD}>
<input
autofocus=true
class="autofocus form-control"
disabled={ !enable }
id="username"
name="username"
oninput={ ctx.link().callback(|e: InputEvent| LoginAppMsg::Input(utils::get_value_from_input_event(e))) }
type="text"
value={ inputvalue }
/>
</div>
<div class={CLASS_DIV_LOGIN_BUTTON}>
<button
type="submit"
class={CLASS_BUTTON_DARK}
disabled={ !enable }
>{" Begin "}</button>
</div>
</form>
</div>
</>
}
}
// Selecting between password (and MFA) or Passkey
LoginState::Select(mechs) => {
html! {
<>
<div class="container">
<p>
{" Which credential would you like to use? "}
</p>
</div>
<div class="container">
<ul class="list-unstyled">
{ for mechs.iter()
.enumerate()
.map(|(idx, mech)| self.render_mech_select(ctx, idx, mech)) }
</ul>
</div>
</>
}
}
LoginState::Continue(allowed) => {
html! {
<>
<div class="container">
<p>
{"Choose how to proceed:"}
</p>
</div>
<div class="container">
<ul class="list-unstyled">
{ for allowed.iter()
.enumerate()
.map(|(idx, allow)| self.render_auth_allowed(ctx, idx, allow)) }
</ul>
</div>
</>
}
}
LoginState::Password(enable) => {
html! {
<>
<div class="container">
<label for="password" class="form-label">{ "Password" }</label>
<form
onsubmit={ ctx.link().callback(|e: FocusEvent| {
console::debug!("login::view_state -> Password - prevent_default()".to_string());
e.prevent_default();
LoginAppMsg::PasswordSubmit
} ) }
>
<div class={CLASS_DIV_LOGIN_FIELD}>
<input
autofocus=true
class="autofocus form-control"
disabled={ !enable }
id="password"
name="password"
oninput={ ctx.link().callback(|e: InputEvent| LoginAppMsg::Input(utils::get_value_from_input_event(e))) }
type="password"
value={ inputvalue }
/>
</div>
<div class={CLASS_DIV_LOGIN_BUTTON}>
<button type="submit" class={CLASS_BUTTON_DARK} disabled={ !enable }>{ "Submit" }</button>
</div>
</form>
</div>
</>
}
}
LoginState::BackupCode(enable) => {
html! {
<>
<div class="container">
<label for="backup_code" class="form-label">
{"Backup Code"}
</label>
<form
onsubmit={ ctx.link().callback(|e: FocusEvent| {
console::debug!("login::view_state -> BackupCode - prevent_default()".to_string());
e.prevent_default();
LoginAppMsg::BackupCodeSubmit
} ) }
>
<div class={CLASS_DIV_LOGIN_FIELD}>
<input
autofocus=true
class="autofocus form-control"
disabled={ !enable }
id="backup_code"
name="backup_code"
oninput={ ctx.link().callback(|e: InputEvent| LoginAppMsg::Input(utils::get_value_from_input_event(e))) }
type="text"
value={ inputvalue }
/>
</div>
<div class={CLASS_DIV_LOGIN_BUTTON}>
<button type="submit" class={CLASS_BUTTON_DARK}>{" Submit "}</button>
</div>
</form>
</div>
</>
}
}
LoginState::Totp(state) => {
html! {
<>
<div class="container">
<label for="totp" class="form-label">{"TOTP"}</label>
<form
onsubmit={ ctx.link().callback(|e: FocusEvent| {
console::debug!("login::view_state -> Totp - prevent_default()".to_string());
e.prevent_default();
LoginAppMsg::TotpSubmit
} ) }
>
<div class={CLASS_DIV_LOGIN_FIELD}>
<input
autofocus=true
class="autofocus form-control"
disabled={ state==&TotpState::Disabled }
id="totp"
name="totp"
oninput={ ctx.link().callback(|e: InputEvent| LoginAppMsg::Input(utils::get_value_from_input_event(e)))}
type="text"
value={ inputvalue }
/>
</div>
<div class={CLASS_DIV_LOGIN_BUTTON}>
<button type="submit" class={CLASS_BUTTON_DARK} disabled={ state==&TotpState::Disabled }>{" Submit "}</button>
</div>
</form>
</div>
</>
}
}
LoginState::SecurityKey(challenge) => {
// Start the navigator parts.
if let Some(win) = web_sys::window() {
let promise = win
.navigator()
.credentials()
.get_with_options(challenge)
.expect_throw("Unable to create promise");
let fut = JsFuture::from(promise);
let linkc = ctx.link().clone();
spawn_local(async move {
match fut.await {
Ok(data) => {
let data = PublicKeyCredential::from(
web_sys::PublicKeyCredential::from(data),
);
linkc.send_message(LoginAppMsg::SecurityKeySubmit(data));
}
Err(e) => {
linkc.send_message(LoginAppMsg::Error {
emsg: format!("{:?}", e),
kopid: None,
});
}
}
});
} else {
ctx.link().send_message(LoginAppMsg::Error {
emsg: "failed to access navigator credentials".to_string(),
kopid: None,
});
};
html! {
<div class="container">
<p>
{"Security Key"}
</p>
</div>
}
}
LoginState::Passkey(challenge) => {
// Start the navigator parts.
if let Some(win) = web_sys::window() {
let promise = win
.navigator()
.credentials()
.get_with_options(challenge)
.expect_throw("Unable to create promise");
let fut = JsFuture::from(promise);
let linkc = ctx.link().clone();
spawn_local(async move {
match fut.await {
Ok(data) => {
let data = PublicKeyCredential::from(
web_sys::PublicKeyCredential::from(data),
);
linkc.send_message(LoginAppMsg::PasskeySubmit(data));
}
Err(e) => {
linkc.send_message(LoginAppMsg::Error {
emsg: format!("{:?}", e),
kopid: None,
});
}
}
});
} else {
ctx.link().send_message(LoginAppMsg::Error {
emsg: "failed to access navigator credentials".to_string(),
kopid: None,
});
};
html! {
<div class="container text-center">
<p>
{"Prompting for Passkey authentication..."}
</p>
</div>
}
}
LoginState::Authenticated => {
let loc = models::pop_return_location();
// redirect
#[cfg(debug)]
console::debug!(format!("authenticated, try going to -> {:?}", loc));
loc.goto(&ctx.link().history().expect_throw("failed to read history"));
html! {
<div class="alert alert-success">
<h3>{ "Login Success 🎉" }</h3>
</div>
}
}
LoginState::Denied(msg) => {
self.do_alert_error("Authentication Denied", Some(msg.as_str()), ctx)
}
LoginState::UnknownUser => {
self.do_alert_error("Username not found", Some("Please try again"), ctx)
}
LoginState::Error { emsg, kopid } => self.do_alert_error(
"An error has occured 😔 ",
Some(
format!(
"{}\n\n{}",
emsg.as_str(),
if let Some(opid) = kopid.as_ref() {
format!("Operation ID: {}", opid.clone())
} else {
"Error occurred client-side.".to_string()
}
)
.as_str(),
),
ctx,
),
}
}
}
impl Component for LoginApp {
type Message = LoginAppMsg;
type Properties = ();
fn create(_ctx: &Context<Self>) -> Self {
#[cfg(debug)]
console::debug!("create".to_string());
// Assume we are here for a good reason.
models::clear_bearer_token();
// Do we have a login hint?
let inputvalue = models::pop_login_hint().unwrap_or_else(|| "".to_string());
#[cfg(debug)]
{
let document = utils::document();
let html_document = document
.dyn_into::<web_sys::HtmlDocument>()
.expect_throw("failed to dyn cast to htmldocument");
let cookie = html_document
.cookie()
.expect_throw("failed to access page cookies");
console::debug!("cookies".to_string());
console::debug!(cookie);
}
// Clean any cookies.
// TODO: actually check that it's cleaning the cookies.
let state = LoginState::Init(true);
add_body_form_classes!();
LoginApp {
inputvalue,
session_id: "".to_string(),
state,
}
}
fn changed(&mut self, _ctx: &Context<Self>) -> bool {
false
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
LoginAppMsg::Input(mut inputvalue) => {
std::mem::swap(&mut self.inputvalue, &mut inputvalue);
true
}
LoginAppMsg::Restart => {
// Clear any leftover input
self.inputvalue = "".to_string();
self.session_id = "".to_string();
self.state = LoginState::Init(true);
true
}
LoginAppMsg::Begin => {
#[cfg(debug)]
console::debug!(format!("begin -> {:?}", self.inputvalue));
// Disable the button?
let username = self.inputvalue.clone();
ctx.link().send_future(async {
match Self::auth_init(username).await {
Ok(v) => v,
Err(v) => v.into(),
}
});
self.state = LoginState::Init(false);
true
}
LoginAppMsg::PasswordSubmit => {
#[cfg(debug)]
console::debug!("At password step".to_string());
// Disable the button?
self.state = LoginState::Password(false);
let authreq = AuthRequest {
step: AuthStep::Cred(AuthCredential::Password(self.inputvalue.clone())),
};
let session_id = self.session_id.clone();
ctx.link().send_future(async {
match Self::auth_step(authreq, session_id).await {
Ok(v) => v,
Err(v) => v.into(),
}
});
// Clear the password from memory.
self.inputvalue = "".to_string();
true
}
LoginAppMsg::BackupCodeSubmit => {
#[cfg(debug)]
console::debug!("backupcode".to_string());
// Disable the button?
self.state = LoginState::BackupCode(false);
let authreq = AuthRequest {
step: AuthStep::Cred(AuthCredential::BackupCode(self.inputvalue.clone())),
};
let session_id = self.session_id.clone();
ctx.link().send_future(async {
match Self::auth_step(authreq, session_id).await {
Ok(v) => v,
Err(v) => v.into(),
}
});
// Clear the backup code from memory.
self.inputvalue = "".to_string();
true
}
LoginAppMsg::TotpSubmit => {
#[cfg(debug)]
console::debug!("totp".to_string());
// Disable the button?
match self.inputvalue.parse::<u32>() {
Ok(totp) => {
self.state = LoginState::Totp(TotpState::Disabled);
let authreq = AuthRequest {
step: AuthStep::Cred(AuthCredential::Totp(totp)),
};
let session_id = self.session_id.clone();
ctx.link().send_future(async {
match Self::auth_step(authreq, session_id).await {
Ok(v) => v,
Err(v) => v.into(),
}
});
}
Err(_) => {
self.state = LoginState::Totp(TotpState::Invalid);
}
}
// Clear the totp from memory.
self.inputvalue = "".to_string();
true
}
LoginAppMsg::SecurityKeySubmit(resp) => {
#[cfg(debug)]
console::debug!("At securitykey step".to_string());
let authreq = AuthRequest {
step: AuthStep::Cred(AuthCredential::SecurityKey(resp)),
};
let session_id = self.session_id.clone();
ctx.link().send_future(async {
match Self::auth_step(authreq, session_id).await {
Ok(v) => v,
Err(v) => v.into(),
}
});
// Do not submit here, we need to wait for the next ui transition.
false
}
LoginAppMsg::PasskeySubmit(resp) => {
#[cfg(debug)]
console::debug!("At passkey step".to_string());
let authreq = AuthRequest {
step: AuthStep::Cred(AuthCredential::Passkey(resp)),
};
let session_id = self.session_id.clone();
ctx.link().send_future(async {
match Self::auth_step(authreq, session_id).await {
Ok(v) => v,
Err(v) => v.into(),
}
});
// Do not submit here, we need to wait for the next ui transition.
false
}
LoginAppMsg::Start(session_id, resp) => {
// Clear any leftover input
self.inputvalue = "".to_string();
#[cfg(debug)]
console::debug!(format!("start -> {:?} : {:?}", resp, session_id));
match resp.state {
AuthState::Choose(mut mechs) => {
self.session_id = session_id;
if mechs.len() == 1 {
// If it's only one mech, just submit that.
let mech = mechs.pop().expect_throw("Memory corruption occured");
let authreq = AuthRequest {
step: AuthStep::Begin(mech),
};
let session_id = self.session_id.clone();
ctx.link().send_future(async {
match Self::auth_step(authreq, session_id).await {
Ok(v) => v,
Err(v) => v.into(),
}
});
// We do NOT need to change state or redraw
false
} else {
#[cfg(debug)]
console::debug!("multiple mechs exist".to_string());
self.state = LoginState::Select(mechs);
true
}
}
AuthState::Denied(reason) => {
#[cfg(debug)]
console::debug!(format!("denied -> {:?}", reason));
self.state = LoginState::Denied(reason);
true
}
_ => {
console::error!("invalid state transition".to_string());
self.state = LoginState::Error {
emsg: "Invalid UI State Transition".to_string(),
kopid: None,
};
true
}
}
}
LoginAppMsg::Select(idx) => {
#[cfg(debug)]
console::debug!(format!("chose -> {:?}", idx));
match &self.state {
LoginState::Select(allowed) => match allowed.get(idx) {
Some(mech) => {
let authreq = AuthRequest {
step: AuthStep::Begin(mech.clone()),
};
let session_id = self.session_id.clone();
ctx.link().send_future(async {
match Self::auth_step(authreq, session_id).await {
Ok(v) => v,
Err(v) => v.into(),
}
});
}
None => {
console::error!("invalid allowed mech idx".to_string());
self.state = LoginState::Error {
emsg: "Invalid Continue Index".to_string(),
kopid: None,
};
}
},
_ => {
console::error!("invalid state transition".to_string());
self.state = LoginState::Error {
emsg: "Invalid UI State Transition".to_string(),
kopid: None,
};
}
};
true
}
LoginAppMsg::Next(resp) => {
// Clear any leftover input
self.inputvalue = "".to_string();
#[cfg(debug)]
console::debug!(format!("next -> {:?}", resp));
// Based on the state we have, we need to chose our steps.
match resp.state {
AuthState::Choose(_mechs) => {
console::error!("invalid state transition".to_string());
self.state = LoginState::Error {
emsg: "Invalid UI State Transition".to_string(),
kopid: None,
};
true
}
AuthState::Continue(mut allowed) => {
if allowed.len() == 1 {
// If there is only one, change our state for that input type.
match allowed.pop().expect_throw("Memory corruption occured") {
AuthAllowed::Anonymous => {
// Just submit this.
}
AuthAllowed::Password => {
// Go to the password view.
self.state = LoginState::Password(true);
}
AuthAllowed::BackupCode => {
self.state = LoginState::BackupCode(true);
}
AuthAllowed::Totp => {
self.state = LoginState::Totp(TotpState::Enabled);
}
AuthAllowed::SecurityKey(challenge) => {
self.state = LoginState::SecurityKey(challenge.into())
}
AuthAllowed::Passkey(challenge) => {
self.state = LoginState::Passkey(challenge.into())
}
}
} else {
// Else, present the options in a choice.
#[cfg(debug)]
console::debug!("multiple choices exist".to_string());
self.state = LoginState::Continue(allowed);
}
true
}
AuthState::Denied(reason) => {
console::error!(format!("denied -> {:?}", reason));
self.state = LoginState::Denied(reason);
true
}
AuthState::Success(bearer_token) => {
// Store the bearer here!
models::set_bearer_token(bearer_token);
self.state = LoginState::Authenticated;
true
}
}
}
LoginAppMsg::Continue(idx) => {
// Are we in the correct internal state?
#[cfg(debug)]
console::debug!(format!("chose -> {:?}", idx));
match &self.state {
LoginState::Continue(allowed) => {
match allowed.get(idx) {
Some(AuthAllowed::Anonymous) => {
// Just submit this.
}
Some(AuthAllowed::Password) => {
// Go to the password view.
self.state = LoginState::Password(true);
}
Some(AuthAllowed::BackupCode) => {
self.state = LoginState::BackupCode(true);
}
Some(AuthAllowed::Totp) => {
self.state = LoginState::Totp(TotpState::Enabled);
}
Some(AuthAllowed::SecurityKey(challenge)) => {
self.state = LoginState::SecurityKey(challenge.clone().into())
}
Some(AuthAllowed::Passkey(challenge)) => {
self.state = LoginState::Passkey(challenge.clone().into())
}
None => {
console::error!("invalid allowed mech idx".to_string());
self.state = LoginState::Error {
emsg: "Invalid Continue Index".to_string(),
kopid: None,
};
}
}
}
_ => {
console::error!("invalid state transition".to_string());
self.state = LoginState::Error {
emsg: "Invalid UI State Transition".to_string(),
kopid: None,
};
}
}
true
}
LoginAppMsg::UnknownUser => {
// Clear any leftover input
self.inputvalue = "".to_string();
console::warn!("Unknown user".to_string());
self.state = LoginState::UnknownUser;
true
}
LoginAppMsg::Error { emsg, kopid } => {
// Clear any leftover input
self.inputvalue = "".to_string();
console::error!(format!("error -> {:?}, {:?}", emsg, kopid));
self.state = LoginState::Error { emsg, kopid };
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
#[cfg(debug)]
console::debug!("login::view".to_string());
// How do we add a top level theme?
/*
let (width, height): (u32, u32) = if let Some(win) = web_sys::window() {
let w = win.inner_width().unwrap();
let h = win.inner_height().unwrap();
ConsoleService::log(format!("width {:?} {:?}", w, w.as_f64()).as_str());
ConsoleService::log(format!("height {:?} {:?}", h, h.as_f64()).as_str());
(w.as_f64().unwrap() as u32, h.as_f64().unwrap() as u32)
} else {
ConsoleService::log("Unable to access document window");
(0, 0)
};
let (width, height) = (width.to_string(), height.to_string());
*/
// <canvas id="confetti-canvas" style="position:absolute" width=width height=height></canvas>
// May need to set these classes?
// <body class="html-body form-body">
// TODO: add the domain_display_name here
html! {
<>
<main class="flex-shrink-0 form-signin">
<center>
<img src="/pkg/img/logo-square.svg" alt="Kanidm" class="kanidm_logo"/>
// TODO: replace this with a call to domain info
<h3>{ "Kanidm" }</h3>
</center>
{ self.view_state(ctx) }
</main>
{ crate::utils::do_footer() }
</>
}
}
fn destroy(&mut self, _ctx: &Context<Self>) {
#[cfg(debug)]
console::debug!("login::destroy".to_string());
remove_body_form_classes!();
}
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
#[cfg(debug)]
console::debug!("login::rendered".to_string());
}
}