Harmonize UI and remove unused css (#3033)

-------

Co-authored-by: Wei Jian Gan <wg@danicapension.dk>
Co-authored-by: William Brown <william@blackhats.net.au>
This commit is contained in:
Wei Jian Gan 2024-10-26 12:47:44 +08:00 committed by GitHub
parent 151a9ad90f
commit bc55313d87
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 1766 additions and 531 deletions

View file

@ -9,12 +9,9 @@ use super::ServerState;
use crate::https::extractors::{DomainInfo, DomainInfoRead}; use crate::https::extractors::{DomainInfo, DomainInfoRead};
// when you want to put big text at the top of the page
pub const CSS_PAGE_HEADER: &str = "d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-0 pb-0 mb-3 border-bottom";
pub const CSS_NAVBAR_NAV: &str = "navbar navbar-expand-md navbar-dark bg-dark mb-4"; pub const CSS_NAVBAR_NAV: &str = "navbar navbar-expand-md navbar-dark bg-dark mb-4";
pub const CSS_NAVBAR_BRAND: &str = "navbar-brand navbar-dark"; pub const CSS_NAVBAR_BRAND: &str = "navbar-brand d-flex align-items-center";
pub const CSS_NAVBAR_LINKS_UL: &str = "navbar-nav me-auto mb-2 mb-md-0"; pub const CSS_NAVBAR_LINKS_UL: &str = "navbar-nav";
pub(crate) fn spa_router_user_ui() -> Router<ServerState> { pub(crate) fn spa_router_user_ui() -> Router<ServerState> {
Router::new() Router::new()

View file

@ -27,7 +27,7 @@
</head> </head>
<body class="flex-column d-flex h-100"> <body class="flex-column d-flex h-100">
<main class="flex-shrink-0 form-signin"> <main class="flex-shrink-0 form-signin m-auto">
<center> <center>
<img <img
src="/pkg/img/logo-square.svg?v={cache_buster_key}" src="/pkg/img/logo-square.svg?v={cache_buster_key}"

View file

@ -10,12 +10,16 @@ use axum_htmx::HxPushUrl;
use kanidm_proto::internal::AppLink; use kanidm_proto::internal::AppLink;
use super::constants::Urls; use super::constants::Urls;
use super::navbar::NavbarCtx;
use crate::https::views::errors::HtmxError; use crate::https::views::errors::HtmxError;
use crate::https::{extractors::VerifiedClientInformation, middleware::KOpId, ServerState}; use crate::https::{
extractors::DomainInfo, extractors::VerifiedClientInformation, middleware::KOpId, ServerState,
};
#[derive(Template)] #[derive(Template)]
#[template(path = "apps.html")] #[template(path = "apps.html")]
struct AppsView { struct AppsView {
navbar_ctx: NavbarCtx,
apps_partial: AppsPartialView, apps_partial: AppsPartialView,
} }
@ -29,6 +33,7 @@ pub(crate) async fn view_apps_get(
State(state): State<ServerState>, State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>, Extension(kopid): Extension<KOpId>,
VerifiedClientInformation(client_auth_info): VerifiedClientInformation, VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
DomainInfo(domain_info): DomainInfo,
) -> axum::response::Result<Response> { ) -> axum::response::Result<Response> {
// Because this is the route where the login page can land, we need to actually alter // Because this is the route where the login page can land, we need to actually alter
// our response as a result. If the user comes here directly we need to render the full // our response as a result. If the user comes here directly we need to render the full
@ -44,6 +49,7 @@ pub(crate) async fn view_apps_get(
( (
HxPushUrl(Uri::from_static(Urls::Apps.as_ref())), HxPushUrl(Uri::from_static(Urls::Apps.as_ref())),
AppsView { AppsView {
navbar_ctx: NavbarCtx { domain_info },
apps_partial: AppsPartialView { apps: app_links }, apps_partial: AppsPartialView { apps: app_links },
}, },
) )

View file

@ -18,6 +18,7 @@ mod constants;
mod cookies; mod cookies;
mod errors; mod errors;
mod login; mod login;
mod navbar;
mod oauth2; mod oauth2;
mod profile; mod profile;
mod reset; mod reset;
@ -94,7 +95,10 @@ pub fn view_router() -> Router<ServerState> {
.route("/reset/add_password", post(reset::view_new_pwd)) .route("/reset/add_password", post(reset::view_new_pwd))
.route("/reset/change_password", post(reset::view_new_pwd)) .route("/reset/change_password", post(reset::view_new_pwd))
.route("/reset/add_passkey", post(reset::view_new_passkey)) .route("/reset/add_passkey", post(reset::view_new_passkey))
.route("/reset/set_unixcred", post(reset::view_set_unixcred))
.route("/api/delete_alt_creds", post(reset::remove_alt_creds)) .route("/api/delete_alt_creds", post(reset::remove_alt_creds))
.route("/api/delete_unixcred", post(reset::remove_unixcred))
.route("/api/add_totp", post(reset::add_totp))
.route("/api/remove_totp", post(reset::remove_totp)) .route("/api/remove_totp", post(reset::remove_totp))
.route("/api/remove_passkey", post(reset::remove_passkey)) .route("/api/remove_passkey", post(reset::remove_passkey))
.route("/api/finish_passkey", post(reset::finish_passkey)) .route("/api/finish_passkey", post(reset::finish_passkey))

View file

@ -0,0 +1,5 @@
use crate::https::extractors::DomainInfoRead;
pub struct NavbarCtx {
pub domain_info: DomainInfoRead,
}

View file

@ -12,10 +12,12 @@ use kanidm_proto::internal::UserAuthToken;
use super::constants::{ProfileMenuItems, UiMessage, Urls}; use super::constants::{ProfileMenuItems, UiMessage, Urls};
use super::errors::HtmxError; use super::errors::HtmxError;
use super::login::{LoginDisplayCtx, Reauth, ReauthPurpose}; use super::login::{LoginDisplayCtx, Reauth, ReauthPurpose};
use super::navbar::NavbarCtx;
#[derive(Template)] #[derive(Template)]
#[template(path = "user_settings.html")] #[template(path = "user_settings.html")]
pub(crate) struct ProfileView { pub(crate) struct ProfileView {
navbar_ctx: NavbarCtx,
profile_partial: ProfilePartialView, profile_partial: ProfilePartialView,
} }
@ -27,13 +29,13 @@ struct ProfilePartialView {
account_name: String, account_name: String,
display_name: String, display_name: String,
email: Option<String>, email: Option<String>,
posix_enabled: bool,
} }
pub(crate) async fn view_profile_get( pub(crate) async fn view_profile_get(
State(state): State<ServerState>, State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>, Extension(kopid): Extension<KOpId>,
VerifiedClientInformation(client_auth_info): VerifiedClientInformation, VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
DomainInfo(domain_info): DomainInfo,
) -> Result<ProfileView, WebError> { ) -> Result<ProfileView, WebError> {
let uat: UserAuthToken = state let uat: UserAuthToken = state
.qe_r_ref .qe_r_ref
@ -45,13 +47,13 @@ pub(crate) async fn view_profile_get(
let can_rw = uat.purpose_readwrite_active(time); let can_rw = uat.purpose_readwrite_active(time);
Ok(ProfileView { Ok(ProfileView {
navbar_ctx: NavbarCtx { domain_info },
profile_partial: ProfilePartialView { profile_partial: ProfilePartialView {
menu_active_item: ProfileMenuItems::UserProfile, menu_active_item: ProfileMenuItems::UserProfile,
can_rw, can_rw,
account_name: uat.name().to_string(), account_name: uat.name().to_string(),
display_name: uat.displayname.clone(), display_name: uat.displayname.clone(),
email: uat.mail_primary.clone(), email: uat.mail_primary.clone(),
posix_enabled: false,
}, },
}) })
} }

View file

@ -16,6 +16,7 @@ use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none; use serde_with::skip_serializing_none;
use std::fmt; use std::fmt;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::str::FromStr;
use uuid::Uuid; use uuid::Uuid;
use kanidm_proto::internal::{ use kanidm_proto::internal::{
@ -25,6 +26,7 @@ use kanidm_proto::internal::{
}; };
use super::constants::Urls; use super::constants::Urls;
use super::navbar::NavbarCtx;
use crate::https::extractors::{DomainInfo, DomainInfoRead, VerifiedClientInformation}; use crate::https::extractors::{DomainInfo, DomainInfoRead, VerifiedClientInformation};
use crate::https::middleware::KOpId; use crate::https::middleware::KOpId;
use crate::https::views::constants::ProfileMenuItems; use crate::https::views::constants::ProfileMenuItems;
@ -37,6 +39,7 @@ use super::UnrecoverableErrorView;
#[derive(Template)] #[derive(Template)]
#[template(path = "user_settings.html")] #[template(path = "user_settings.html")]
struct ProfileView { struct ProfileView {
navbar_ctx: NavbarCtx,
profile_partial: CredStatusView, profile_partial: CredStatusView,
} }
@ -62,7 +65,6 @@ struct CredStatusView {
menu_active_item: ProfileMenuItems, menu_active_item: ProfileMenuItems,
names: String, names: String,
credentials_update_partial: CredResetPartialView, credentials_update_partial: CredResetPartialView,
posix_enabled: bool,
} }
#[derive(Template)] #[derive(Template)]
@ -76,6 +78,8 @@ struct CredResetPartialView {
attested_passkeys: Vec<PasskeyDetail>, attested_passkeys: Vec<PasskeyDetail>,
passkeys: Vec<PasskeyDetail>, passkeys: Vec<PasskeyDetail>,
primary: Option<CredentialDetail>, primary: Option<CredentialDetail>,
unixcred_state: CUCredState,
unixcred: Option<CredentialDetail>,
} }
#[skip_serializing_none] #[skip_serializing_none]
@ -91,6 +95,12 @@ struct AddPasswordPartial {
check_res: PwdCheckResult, check_res: PwdCheckResult,
} }
#[derive(Template)]
#[template(path = "credential_update_set_unixcred_partial.html")]
struct SetUnixCredPartial {
check_res: PwdCheckResult,
}
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
enum PwdCheckResult { enum PwdCheckResult {
Success, Success,
@ -111,7 +121,7 @@ pub(crate) struct NewPassword {
pub(crate) struct NewTotp { pub(crate) struct NewTotp {
name: String, name: String,
#[serde(rename = "checkTOTPCode")] #[serde(rename = "checkTOTPCode")]
check_totpcode: u32, check_totpcode: String,
#[serde(rename = "ignoreBrokenApp")] #[serde(rename = "ignoreBrokenApp")]
ignore_broken_app: bool, ignore_broken_app: bool,
} }
@ -154,44 +164,28 @@ pub(crate) struct TOTPRemoveData {
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub(crate) enum TotpCheckResult { pub(crate) struct TotpInit {
Init { secret: String,
secret: String, qr_code_svg: String,
qr_code_svg: String, steps: u64,
steps: u64, digits: u8,
digits: u8, algo: TotpAlgo,
algo: TotpAlgo, uri: String,
uri: String,
},
Failure {
wrong_code: bool,
broken_app: bool,
warnings: Vec<TotpFeedback>,
},
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Serialize, Deserialize, Debug, Default)]
pub(crate) enum TotpFeedback { pub(crate) struct TotpCheck {
BlankName, wrong_code: bool,
DuplicateName, broken_app: bool,
}
impl Display for TotpFeedback {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
TotpFeedback::BlankName => write!(f, "Please enter a name."),
TotpFeedback::DuplicateName => write!(
f,
"This name already exists, choose another or remove the existing one."
),
}
}
} }
#[derive(Template)] #[derive(Template)]
#[template(path = "credential_update_add_totp_partial.html")] #[template(path = "credential_update_add_totp_partial.html")]
struct AddTotpPartial { struct AddTotpPartial {
check_res: TotpCheckResult, totp_init: Option<TotpInit>,
totp_name: String,
totp_value: String,
check: TotpCheck,
} }
#[derive(PartialEq, Debug, Serialize, Deserialize)] #[derive(PartialEq, Debug, Serialize, Deserialize)]
@ -285,6 +279,28 @@ pub(crate) async fn remove_alt_creds(
Ok(get_cu_partial_response(cu_status)) Ok(get_cu_partial_response(cu_status))
} }
pub(crate) async fn remove_unixcred(
State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>,
HxRequest(_hx_request): HxRequest,
VerifiedClientInformation(_client_auth_info): VerifiedClientInformation,
jar: CookieJar,
) -> axum::response::Result<Response> {
let cu_session_token: CUSessionToken = get_cu_session(jar).await?;
let cu_status = state
.qe_r_ref
.handle_idmcredentialupdate(
cu_session_token,
CURequest::UnixPasswordRemove,
kopid.eventid,
)
.map_err(|op_err| HtmxError::new(&kopid, op_err))
.await?;
Ok(get_cu_partial_response(cu_status))
}
pub(crate) async fn remove_totp( pub(crate) async fn remove_totp(
State(state): State<ServerState>, State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>, Extension(kopid): Extension<KOpId>,
@ -428,63 +444,64 @@ pub(crate) async fn view_new_totp(
HxRequest(_hx_request): HxRequest, HxRequest(_hx_request): HxRequest,
VerifiedClientInformation(_client_auth_info): VerifiedClientInformation, VerifiedClientInformation(_client_auth_info): VerifiedClientInformation,
jar: CookieJar, jar: CookieJar,
opt_form: Option<Form<NewTotp>>,
) -> axum::response::Result<Response> { ) -> axum::response::Result<Response> {
let cu_session_token = get_cu_session(jar).await?; let cu_session_token = get_cu_session(jar).await?;
let push_url = HxPushUrl(Uri::from_static("/ui/reset/add_totp")); let push_url = HxPushUrl(Uri::from_static("/ui/reset/add_totp"));
let swapped_handler_trigger =
HxResponseTrigger::after_swap([HxEvent::new("addTotpSwapped".to_string())]);
let new_totp = match opt_form { let cu_status = state
// Initial response handling, user is entering the form for first time .qe_r_ref
None => { .handle_idmcredentialupdate(cu_session_token, CURequest::TotpGenerate, kopid.eventid)
let cu_status = state .await
.qe_r_ref // TODO: better handling for invalid mfaregstate state, can be invalid if certain mfa flows were interrupted
.handle_idmcredentialupdate( // TODO: We should maybe automatically cancel the other MFA reg
cu_session_token, .map_err(|op_err| HtmxError::new(&kopid, op_err))?;
CURequest::TotpGenerate,
kopid.eventid,
)
.await
// TODO: better handling for invalid mfaregstate state, can be invalid if certain mfa flows were interrupted
// TODO: We should maybe automatically cancel the other MFA reg
.map_err(|op_err| HtmxError::new(&kopid, op_err))?;
let partial = if let CURegState::TotpCheck(secret) = cu_status.mfaregstate { let partial = if let CURegState::TotpCheck(secret) = cu_status.mfaregstate {
let uri = secret.to_uri(); let uri = secret.to_uri();
let svg = match QrCode::new(uri.as_str()) { let svg = match QrCode::new(uri.as_str()) {
Ok(qr) => qr.render::<svg::Color>().build(), Ok(qr) => qr.render::<svg::Color>().build(),
Err(qr_err) => { Err(qr_err) => {
error!("Failed to create TOTP QR code: {qr_err}"); error!("Failed to create TOTP QR code: {qr_err}");
"QR Code Generation Failed".to_string() "QR Code Generation Failed".to_string()
} }
}; };
AddTotpPartial { AddTotpPartial {
check_res: TotpCheckResult::Init { totp_init: Some(TotpInit {
secret: secret.get_secret(), secret: secret.get_secret(),
qr_code_svg: svg, qr_code_svg: svg,
steps: secret.step, steps: secret.step,
digits: secret.digits, digits: secret.digits,
algo: secret.algo, algo: secret.algo,
uri, uri,
}, }),
} totp_name: Default::default(),
} else { totp_value: Default::default(),
return Err(ErrorResponse::from(HtmxError::new( check: TotpCheck::default(),
&kopid,
OperationError::CannotStartMFADuringOngoingMFASession,
)));
};
return Ok((swapped_handler_trigger, push_url, partial).into_response());
} }
} else {
// User has submitted a totp code return Err(ErrorResponse::from(HtmxError::new(
Some(Form(new_totp)) => new_totp, &kopid,
OperationError::CannotStartMFADuringOngoingMFASession,
)));
}; };
let cu_status = if new_totp.ignore_broken_app { Ok((push_url, partial).into_response())
}
pub(crate) async fn add_totp(
State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>,
HxRequest(_hx_request): HxRequest,
VerifiedClientInformation(_client_auth_info): VerifiedClientInformation,
jar: CookieJar,
new_totp_form: Form<NewTotp>,
) -> axum::response::Result<Response> {
let cu_session_token = get_cu_session(jar).await?;
let check_totpcode = u32::from_str(&new_totp_form.check_totpcode).unwrap_or_default();
let cu_status = if new_totp_form.ignore_broken_app {
// Cope with SHA1 apps because the user has intended to do so, their totp code was already verified // Cope with SHA1 apps because the user has intended to do so, their totp code was already verified
state.qe_r_ref.handle_idmcredentialupdate( state.qe_r_ref.handle_idmcredentialupdate(
cu_session_token, cu_session_token,
@ -495,25 +512,22 @@ pub(crate) async fn view_new_totp(
// Validate totp code example // Validate totp code example
state.qe_r_ref.handle_idmcredentialupdate( state.qe_r_ref.handle_idmcredentialupdate(
cu_session_token, cu_session_token,
CURequest::TotpVerify(new_totp.check_totpcode, new_totp.name), CURequest::TotpVerify(check_totpcode, new_totp_form.name.clone()),
kopid.eventid, kopid.eventid,
) )
} }
.await .await
.map_err(|op_err| HtmxError::new(&kopid, op_err))?; .map_err(|op_err| HtmxError::new(&kopid, op_err))?;
let warnings = vec![]; let check = match &cu_status.mfaregstate {
let check_res = match &cu_status.mfaregstate {
CURegState::None => return Ok(get_cu_partial_response(cu_status)), CURegState::None => return Ok(get_cu_partial_response(cu_status)),
CURegState::TotpTryAgain => TotpCheckResult::Failure { CURegState::TotpTryAgain => TotpCheck {
wrong_code: true, wrong_code: true,
broken_app: false, ..Default::default()
warnings,
}, },
CURegState::TotpInvalidSha1 => TotpCheckResult::Failure { CURegState::TotpInvalidSha1 => TotpCheck {
wrong_code: false,
broken_app: true, broken_app: true,
warnings, ..Default::default()
}, },
CURegState::TotpCheck(_) CURegState::TotpCheck(_)
| CURegState::BackupCodes(_) | CURegState::BackupCodes(_)
@ -526,10 +540,23 @@ pub(crate) async fn view_new_totp(
} }
}; };
let check_totpcode = if check.wrong_code {
String::default()
} else {
new_totp_form.check_totpcode.clone()
};
let swapped_handler_trigger =
HxResponseTrigger::after_swap([HxEvent::new("addTotpSwapped".to_string())]);
Ok(( Ok((
swapped_handler_trigger, swapped_handler_trigger,
push_url, AddTotpPartial {
AddTotpPartial { check_res }, totp_init: None,
totp_name: new_totp_form.name.clone(),
totp_value: check_totpcode,
check,
},
) )
.into_response()) .into_response())
} }
@ -658,6 +685,66 @@ fn add_cu_cookie(
jar.add(token_cookie) jar.add(token_cookie)
} }
pub(crate) async fn view_set_unixcred(
State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>,
HxRequest(_hx_request): HxRequest,
VerifiedClientInformation(_client_auth_info): VerifiedClientInformation,
jar: CookieJar,
opt_form: Option<Form<NewPassword>>,
) -> axum::response::Result<Response> {
let cu_session_token: CUSessionToken = get_cu_session(jar).await?;
let swapped_handler_trigger =
HxResponseTrigger::after_swap([HxEvent::new("addPasswordSwapped".to_string())]);
let new_passwords = match opt_form {
None => {
return Ok((
swapped_handler_trigger,
SetUnixCredPartial {
check_res: PwdCheckResult::Init,
},
)
.into_response());
}
Some(Form(new_passwords)) => new_passwords,
};
let pwd_equal = new_passwords.new_password == new_passwords.new_password_check;
let (warnings, status) = if pwd_equal {
let res = state
.qe_r_ref
.handle_idmcredentialupdate(
cu_session_token,
CURequest::UnixPassword(new_passwords.new_password),
kopid.eventid,
)
.await;
match res {
Ok(cu_status) => return Ok(get_cu_partial_response(cu_status)),
Err(OperationError::PasswordQuality(password_feedback)) => {
(password_feedback, StatusCode::UNPROCESSABLE_ENTITY)
}
Err(operr) => return Err(ErrorResponse::from(HtmxError::new(&kopid, operr))),
}
} else {
(vec![], StatusCode::UNPROCESSABLE_ENTITY)
};
let check_res = PwdCheckResult::Failure {
pwd_equal,
warnings,
};
Ok((
status,
swapped_handler_trigger,
HxPushUrl(Uri::from_static("/ui/reset/set_unixcred")),
AddPasswordPartial { check_res },
)
.into_response())
}
pub(crate) async fn view_reset_get( pub(crate) async fn view_reset_get(
State(state): State<ServerState>, State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>, Extension(kopid): Extension<KOpId>,
@ -758,6 +845,8 @@ fn get_cu_partial(cu_status: CUStatus) -> CredResetPartialView {
passkeys, passkeys,
primary_state, primary_state,
primary, primary,
unixcred_state,
unixcred,
.. ..
} = cu_status; } = cu_status;
@ -770,6 +859,8 @@ fn get_cu_partial(cu_status: CUStatus) -> CredResetPartialView {
passkeys, passkeys,
primary_state, primary_state,
primary, primary,
unixcred_state,
unixcred,
} }
} }
@ -799,16 +890,15 @@ fn get_cu_response(
if is_logged_in { if is_logged_in {
let cred_status_view = CredStatusView { let cred_status_view = CredStatusView {
menu_active_item: ProfileMenuItems::Credentials, menu_active_item: ProfileMenuItems::Credentials,
domain_info, domain_info: domain_info.clone(),
names, names,
credentials_update_partial, credentials_update_partial,
// TODO: fill in posix enabled
posix_enabled: false,
}; };
( (
HxPushUrl(Uri::from_static(Urls::UpdateCredentials.as_ref())), HxPushUrl(Uri::from_static(Urls::UpdateCredentials.as_ref())),
ProfileView { ProfileView {
navbar_ctx: NavbarCtx { domain_info },
profile_partial: cred_status_view, profile_partial: cred_status_view,
}, },
) )

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 839 KiB

View file

@ -69,7 +69,7 @@ function onPasskeyCreated(assertion) {
document.getElementById("passkey-create-data").value = JSON.stringify(creationData) document.getElementById("passkey-create-data").value = JSON.stringify(creationData)
// Make the name input visible and hide the "Begin Passkey Enrollment" button // Make the name input visible and hide the "Begin Passkey Enrollment" button
document.getElementById("passkeyNamingSafariBtn").classList.add("d-none") document.getElementById("passkeyNamingSafariPre").classList.add("d-none")
document.getElementById("passkeyNamingForm").classList.remove("d-none") document.getElementById("passkeyNamingForm").classList.remove("d-none")
document.getElementById("passkeyNamingSubmitBtn").classList.remove("d-none") document.getElementById("passkeyNamingSubmitBtn").classList.remove("d-none")
} catch (e) { } catch (e) {

View file

@ -1,124 +1,52 @@
:root {
--totp-width-and-height: 30px;
--totp-stroke-width: 60px;
}
html, html,
body { body {
height: 100%; height: 100%;
} }
.input-hidden {
display: none;
}
.form-cred-reset-body { .form-cred-reset-body {
width: 100%;
max-width: 500px; max-width: 500px;
padding: 15px;
margin: auto;
} }
#settings-window:has(.form-cred-reset-body) .form-cred-reset-body { #settings-window .form-cred-reset-body {
max-width: unset; max-width: unset;
padding: unset;
}
.form-signin-body {
display: flex;
align-items: center;
padding-top: 40px;
padding-bottom: 40px;
background-color: #f5f5f5;
} }
.form-signin { .form-signin {
max-width: 680px; max-width: 680px;
margin: auto;
}
.form-signin .checkbox {
font-weight: 400;
}
.form-signin .form-floating:focus-within {
z-index: 2;
}
.form-signin input[type="email"] {
margin-bottom: -1px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
/* DASHBOARD */
.dash-body {
font-size: 0.875rem;
}
.feather {
width: 16px;
height: 16px;
vertical-align: text-bottom;
} }
/* /*
* Sidebar * Sidebar
*/ */
.sidebar { .side-menu {
position: fixed; min-width: 180px;
top: 0;
/* rtl:raw:
right: 0;
*/
bottom: 0;
/* rtl:remove */
left: 0;
z-index: 100; /* Behind the navbar */
padding: 48px 0 0; /* Height of navbar */
box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.1);
} }
@media (max-width: 767.98px) { .side-menu-item {
.sidebar { --icon-size: 24px;
top: 5rem; padding: .4rem .7rem;
text-decoration: none;
&.active {
font-weight: 600;
}
&:hover, &.active {
background-color: var(--bs-gray-300);
} }
}
.sidebar-sticky { .icon-container img {
position: relative; filter: invert(40%);
top: 0; width: 100%;
height: calc(100vh - 48px); height: 100%;
padding-top: 0.5rem; object-fit: contain;
overflow-x: hidden; }
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
}
.sidebar .nav-link {
font-weight: 500;
color: #333;
}
.sidebar .nav-link .feather {
margin-right: 4px;
color: #727272;
}
.sidebar .nav-link.active {
color: #2470dc;
}
.sidebar .nav-link:hover .feather,
.sidebar .nav-link.active .feather {
color: inherit;
}
.sidebar-heading {
font-size: 0.75rem;
text-transform: uppercase;
} }
/* /*
@ -130,49 +58,6 @@ body {
* Navbar * Navbar
*/ */
.navbar-brand {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
padding-left: 1rem;
padding-right: 2rem;
font-size: 1.25rem;
}
.navbar .navbar-toggler {
top: 0.25rem;
right: 1rem;
}
.navbar-toggler-img {
width: 50px;
height: 50px;
padding: 0px;
margin: 0px;
}
.navbar .form-control {
padding: 0.75rem 1rem;
border-width: 0;
border-radius: 0;
}
.form-control-dark {
color: #fff;
background-color: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.1);
}
.form-control-dark:focus {
border-color: transparent;
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.25);
}
.vert-center {
height: 80%;
display: flex;
align-items: center;
}
.kanidm_logo { .kanidm_logo {
width: 12em; width: 12em;
height: 12em; height: 12em;
@ -186,11 +71,6 @@ body {
margin: auto; margin: auto;
} }
:root {
--totp-width-and-height: 30px;
--totp-stroke-width: 60px;
}
.totp-display-container { .totp-display-container {
padding: 5px 10px; padding: 5px 10px;
display: flex; display: flex;
@ -264,15 +144,25 @@ body {
min-height: 150px; min-height: 150px;
} }
#graph-container svg { .btn-tiny {
max-width: 100%; --bs-btn-padding-y: .05rem;
height: fit-content; --bs-btn-padding-x: .4rem;
--bs-btn-font-size: .75rem;
} }
#cred-update-commit-bar { #cred-update-commit-bar {
display: block; display: block;
/*
position: fixed; position: fixed;
bottom: .5rem;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
*/
background: white; background: white;
} }
.icon-container {
padding: 2px;
width: var(--icon-size);
height: var(--icon-size);
}

View file

@ -1,9 +1,10 @@
<main class="p-3 x-auto"> <main class="container-lg">
<div class="(( crate::https::ui::CSS_PAGE_HEADER ))"> <div>
<h2>Applications list</h2> <h2>Applications list</h2>
</div> </div>
<hr />
(% if apps.is_empty() %) (% if apps.is_empty() %)
<h5>No linked applications available</h5> <p>No linked applications available</p>
(% else %) (% else %)
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3"> <div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3">
(% for app in apps %) (% for app in apps %)

View file

@ -5,3 +5,4 @@
(% block main %)(% endblock %) (% block main %)(% endblock %)
(% include "signout_modal.html" %) (% include "signout_modal.html" %)
(% endblock %) (% endblock %)

View file

@ -3,7 +3,12 @@
<script id="data">(( challenge|safe ))</script> <script id="data">(( challenge|safe ))</script>
<!-- Safari requires a human input to start passkey creation --> <!-- Safari requires a human input to start passkey creation -->
<button id="passkeyNamingSafariBtn" class="btn btn-primary">Begin Passkey Enrolment</button> <div id="passkeyNamingSafariPre">
<button id="passkeyNamingSafariBtn" class="btn btn-primary">Begin Passkey Enrolment</button>
<div class="d-flex justify-content-end pt-3 g-3" hx-target="#credentialUpdateDynamicSection">
<button id="password-cancel" type="button" class="btn btn-danger" hx-post="/ui/api/cancel_mfareg">Cancel</button>
</div>
</div>
<form id="passkeyNamingForm" class="g-2 d-none"> <form id="passkeyNamingForm" class="g-2 d-none">
<b>Adding a new passkey</b> <b>Adding a new passkey</b>

View file

@ -54,7 +54,7 @@
</div> </div>
</form> </form>
<div class="g-3 d-flex justify-content-end" hx-target="#credentialUpdateDynamicSection"> <div class="g-3 d-flex justify-content-end" hx-target="#credentialUpdateDynamicSection">
<button id="password-cancel" type="button" class="btn btn-danger me-2" hx-get=(Urls::CredReset) hx-target="body">Cancel</button> <button id="password-cancel" type="button" class="btn btn-danger me-2" hx-get=((Urls::CredReset)) hx-target="body">Cancel</button>
<button id="password-submit" type="button" class="btn btn-primary" <button id="password-submit" type="button" class="btn btn-primary"
hx-post="/ui/reset/add_password" hx-post="/ui/reset/add_password"
hx-include="#newPasswordForm" hx-include="#newPasswordForm"

View file

@ -1,6 +1,6 @@
<div> <div>
<div id="totpInfo"> <div id="totpInfo">
(% if let TotpCheckResult::Init with { secret, qr_code_svg, steps, digits, algo, uri } = check_res %) (% if let Some(TotpInit with { secret, qr_code_svg, steps, digits, algo, uri }) = totp_init %)
<div>((qr_code_svg|safe)) <div>((qr_code_svg|safe))
</div> </div>
<code>((uri|safe))</code> <code>((uri|safe))</code>
@ -13,86 +13,67 @@
<li>Code size: (( digits )) digits</li> <li>Code size: (( digits )) digits</li>
</ul> </ul>
(% endif %) (% endif %)
</div> </div>
<form class="row g-2 pb-3 needs-validation" id="newTotpForm" novalidate>
(% let potentially_invalid_name_class = "" %)
(% let potentially_invalid_check_class = "" %)
(% let wrong_code = false %)
(% let broken_app = false %)
(% if let TotpCheckResult::Failure with { wrong_code, broken_app, warnings } = check_res %)
(% let wrong_code = wrong_code.clone() %)
(% let broken_app = broken_app.clone() %)
(% if !warnings.is_empty() %)
(% let potentially_invalid_name_class = "is-invalid" %)
(% endif %)
(% if wrong_code %)
(% let potentially_invalid_check_class = "is-invalid" %)
(% endif %)
(% endif %)
<label for="new-totp-name" class="form-label">Enter a name for your TOTP</label> <div id="newTotpForm">
<input <form class="row g-2 pb-3 needs-validation" novalidate>
aria-describedby="totp-name-validation-feedback" <label for="new-totp-name" class="form-label">Enter a name for your TOTP</label>
class="form-control ((potentially_invalid_name_class))" <input
name="name" aria-describedby="totp-name-validation-feedback"
id="new-totp-name" class="form-control"
required name="name"
autofocus id="new-totp-name"
/> value="(( totp_name ))"
<!-- bootstrap hides the feedback if we remove is-invalid from the input above --> required
(% if let TotpCheckResult::Failure with { wrong_code, broken_app, warnings } = check_res %) autofocus
<div id="totp-name-validation-feedback" class="invalid-feedback d-block"> />
<ul>
(% for warn in warnings %)
<li>(( warn ))</li>
(% endfor %)
</ul>
</div>
(% endif %)
<label for="new-totp-check" class="form-label">Enter a TOTP code to confirm it's working</label> <label for="new-totp-check" class="form-label">Enter a TOTP code to confirm it's working</label>
<input <input
aria-describedby="new-totp-check-feedback" aria-describedby="new-totp-check-feedback"
class="form-control ((potentially_invalid_check_class))" class="form-control (%- if check.broken_app || check.wrong_code -%)is-invalid(%- endif -%)"
name="checkTOTPCode" name="checkTOTPCode"
id="new-totp-check" id="new-totp-check"
type="number" value="(( totp_value ))"
required type="number"
/> required
(% if broken_app || wrong_code %) />
<div id="neq-totp-validation-feedback" class="invalid-feedback">
<ul> (% if check.broken_app %)
(% if wrong_code %) <div id="neq-totp-validation-feedback">
<li>Incorrect TOTP code - Please try again</li> <ul>
(% endif %)
(% if broken_app %)
<li>Your authenticator appears to be implemented in a way that uses SHA1, rather than SHA256. Are you sure you want to proceed? If you want to try with a new authenticator, enter a new code.</li> <li>Your authenticator appears to be implemented in a way that uses SHA1, rather than SHA256. Are you sure you want to proceed? If you want to try with a new authenticator, enter a new code.</li>
(% endif %) </ul>
</ul> </div>
</div> (% else if check.wrong_code %)
(% endif %) <div id="neq-totp-validation-feedback">
<ul>
<li>Incorrect TOTP code - Please try again</li>
</ul>
</div>
(% endif %)
</form> </form>
<div class="g-3 d-flex justify-content-end" hx-target="#credentialUpdateDynamicSection"> <div class="g-3 d-flex justify-content-end" hx-target="#credentialUpdateDynamicSection">
<button id="totp-cancel" type="button" class="btn btn-danger me-2" hx-post="/ui/api/cancel_mfareg">Cancel</button> <button id="totp-cancel" type="button" class="btn btn-danger me-2" hx-post="/ui/api/cancel_mfareg">Cancel</button>
(% if broken_app %) (% if check.broken_app %)
<button id="totp-submit" type="button" class="btn btn-warning" <button id="totp-submit" type="button" class="btn btn-warning"
hx-post="/ui/reset/add_totp" hx-post="/ui/api/add_totp"
hx-target="#newTotpForm" hx-target="#newTotpForm"
hx-select="#newTotpForm > *" hx-select="#newTotpForm > *"
hx-vals='{"ignoreBrokenApp": true}' hx-vals='{"ignoreBrokenApp": true}'
hx-include="#newTotpForm" hx-include="#newTotpForm"
>Accept SHA1</button> >Accept SHA1</button>
(% else %) (% else %)
<button id="totp-submit" type="button" class="btn btn-primary" <button id="totp-submit" type="button" class="btn btn-primary"
hx-post="/ui/reset/add_totp" hx-post="/ui/api/add_totp"
hx-target="#newTotpForm" hx-target="#newTotpForm"
hx-select="#newTotpForm > *" hx-select="#newTotpForm > *"
hx-vals='{"ignoreBrokenApp": false}' hx-vals='{"ignoreBrokenApp": false}'
hx-include="#newTotpForm" hx-include="#newTotpForm"
>Add</button> >Add</button>
(% endif %) (% endif %)
</div>
</div> </div>
</div> </div>

View file

@ -0,0 +1,64 @@
<div>
<form class="row g-2 pb-3 needs-validation" id="newPasswordForm" novalidate>
<input hidden type="text" autocomplete="username" />
(% let potentially_invalid_input_class = "" %)
(% let potentially_invalid_reinput_class = "" %)
(% let pwd_equal = true %)
(% if let PwdCheckResult::Failure with { pwd_equal, warnings } = check_res %)
(% let pwd_equal = pwd_equal.clone() %)
(% if !warnings.is_empty() %)
(% let potentially_invalid_input_class = "is-invalid" %)
(% endif %)
(% if pwd_equal %)
(% let potentially_invalid_reinput_class = "is-invalid" %)
(% endif %)
(% endif %)
<label for="new-password" class="form-label">Enter New Password</label>
<input
aria-describedby="password-validation-feedback"
autocomplete="new-password"
class="form-control ((potentially_invalid_input_class))"
name="new_password"
id="new-password"
placeholder=""
type="password"
required
autofocus
/>
<!-- bootstrap hides the feedback if we remove is-invalid from the input above -->
(% if let PwdCheckResult::Failure with { pwd_equal, warnings } = check_res %)
<div id="password-validation-feedback" class="invalid-feedback d-block">
<ul>
(% for warn in warnings %)
<li>(( warn ))</li>
(% endfor %)
</ul>
</div>
(% endif %)
<label for="new-password-check" class="form-label">Repeat Password</label>
<input
aria-describedby="neq-password-validation-feedback"
autocomplete="new-password"
class="form-control ((potentially_invalid_reinput_class))"
name="new_password_check"
id="new-password-check"
placeholder=""
type="password"
required
/>
<div id="neq-password-validation-feedback" class="invalid-feedback">
<ul><li>Passwords don't match</li></ul>
</div>
</form>
<div class="g-3 d-flex justify-content-end" hx-target="#credentialUpdateDynamicSection">
<button id="password-cancel" type="button" class="btn btn-danger me-2" hx-get=((Urls::CredReset)) hx-target="body">Cancel</button>
<button id="password-submit" type="button" class="btn btn-primary"
hx-post="/ui/reset/set_unixcred"
hx-include="#newPasswordForm"
>Submit</button>
</div>
</div>

View file

@ -5,14 +5,13 @@
(% endblock %) (% endblock %)
(% block body %) (% block body %)
<div class="d-flex align-items-start form-cred-reset-body"> <main class="form-cred-reset-body container my-auto">
<main class="w-100"> <div class="text-center">
<div class="py-3 text-center"> <h3>Updating Credentials</h3>
<h3>Updating Credentials</h3> <p><strong>User:</strong> (( names ))</p>
<p>(( names ))</p> <p><strong>Kanidm domain:</strong> (( domain_info.display_name() ))</p>
<p>(( domain_info.display_name() ))</p> </div>
</div> <hr class="my-4" />
(( credentials_update_partial|safe )) (( credentials_update_partial|safe ))
</main> </main>
</div>
(% endblock %) (% endblock %)

View file

@ -12,7 +12,7 @@
(% endblock %) (% endblock %)
(% block body %) (% block body %)
<main class="flex-shrink-0 container form-signin" id="cred-reset-form"> <main class="flex-shrink-0 container form-signin m-auto" id="cred-reset-form">
<center> <center>
<img <img
src="/pkg/img/logo-square.svg?v=((crate::https::cache_buster::get_cache_buster_key()))" src="/pkg/img/logo-square.svg?v=((crate::https::cache_buster::get_cache_buster_key()))"

View file

@ -3,13 +3,11 @@
(% block settings_window %) (% block settings_window %)
<div class="d-flex align-items-start form-cred-reset-body"> <div class="form-cred-reset-body">
<div class="w-100"> <div>
<div class="py-3"> <p><strong>User:</strong> (( names ))</p>
<p><strong>User:</strong> (( names ))</p> <p><strong>Kanidm domain:</strong> (( domain_info.display_name() ))</p>
<p><strong>Kanidm domain:</strong> (( domain_info.display_name() ))</p>
</div>
(( credentials_update_partial|safe ))
</div> </div>
(( credentials_update_partial|safe ))
</div> </div>
(% endblock %) (% endblock %)

View file

@ -1,16 +1,20 @@
<hr class="my-4" /> <hr class="my-4" />
<h4>Attested Passkeys</h4> <h4>Attested Passkeys</h4>
<p>Passkeys originating from a signed authenticator.</p> <p>Passkeys originating from a signed authenticator.</p>
(% for passkey in attested_passkeys %) <ul class="list-group">
<div class="row mb-3"> (% for passkey in attested_passkeys %)
<div class="col d-flex align-items-center"><span>(( passkey.tag ))</span></div> <li class="list-group-item">
<div class="col d-flex justify-content-end"> <div class="d-flex justify-content-between">
<button type="button" class="btn btn-danger btn-sml" id="(( passkey.tag ))" <div>(( passkey.tag ))</div>
<div>
<button type="button" class="btn btn-danger btn-sml" id="(( passkey.tag ))"
hx-target="#credentialUpdateDynamicSection" hx-target="#credentialUpdateDynamicSection"
hx-confirm="Are you sure you want to delete attested passkey (( passkey.tag )) ?" hx-confirm="Are you sure you want to delete attested passkey (( passkey.tag )) ?"
hx-post="/ui/api/remove_passkey" hx-vals='{"uuid": "(( passkey.uuid ))"}'> hx-post="/ui/api/remove_passkey" hx-vals='{"uuid": "(( passkey.uuid ))"}'>
Remove Remove
</button> </button>
</div>
</div> </div>
</div> </li>
(% endfor %) (% endfor %)
</ul>

View file

@ -5,24 +5,23 @@
src="/pkg/external/base64.js?v=((crate::https::cache_buster::get_cache_buster_key()))" src="/pkg/external/base64.js?v=((crate::https::cache_buster::get_cache_buster_key()))"
async></script> async></script>
<div class="row g-3" id="credentialUpdateDynamicSection" <div id="credentialUpdateDynamicSection"
hx-on::before-swap="stillSwapFailureResponse(event)"> hx-on::before-swap="stillSwapFailureResponse(event)">
<form class="needs-validation mb-5 pb-5" novalidate> <form class="needs-validation mb-5 pb-5" novalidate>
(% match ext_cred_portal %) (% match ext_cred_portal %)
(% when CUExtPortal::None %) (% when CUExtPortal::None %)
(% when CUExtPortal::Hidden %) (% when CUExtPortal::Hidden %)
<hr class="my-4" />
<p>This account is externally managed. Some features may not be <p>This account is externally managed. Some features may not be
available.</p> available.</p>
(% when CUExtPortal::Some(url) %)
<hr class="my-4" /> <hr class="my-4" />
(% when CUExtPortal::Some(url) %)
<p>This account is externally managed. Some features may not be <p>This account is externally managed. Some features may not be
available.</p> available.</p>
<a href="(( url ))">Visit the external account portal</a> <a href="(( url ))">Visit the external account portal</a>
<hr class="my-4" />
(% endmatch %) (% endmatch %)
(% if warnings.len() > 0 %) (% if warnings.len() > 0 %)
<hr class="my-4">
(% for warning in warnings %) (% for warning in warnings %)
(% let is_danger = [CURegWarning::WebauthnAttestationUnsatisfiable, (% let is_danger = [CURegWarning::WebauthnAttestationUnsatisfiable,
CURegWarning::Unsatisfiable].contains(warning) %) CURegWarning::Unsatisfiable].contains(warning) %)
@ -57,6 +56,7 @@
(% endif %) (% endif %)
</div> </div>
(% endfor %) (% endfor %)
<hr class="my-4" />
(% endif %) (% endif %)
<!-- Attested Passkeys --> <!-- Attested Passkeys -->
@ -81,15 +81,15 @@
(% when CUCredState::Modifiable %) (% when CUCredState::Modifiable %)
(% include "credentials_update_passkeys.html" %) (% include "credentials_update_passkeys.html" %)
<!-- Here we are modifiable so we can render the button to add passkeys --> <!-- Here we are modifiable so we can render the button to add passkeys -->
<div class="btn-group"> <div class="btn-group mt-4">
<button type="button" class="btn btn-primary" <button type="button" class="btn btn-secondary"
hx-post="/ui/reset/add_passkey" hx-post="/ui/reset/add_passkey"
hx-vals='{"class": "Any"}' hx-vals='{"class": "Any"}'
hx-target="#credentialUpdateDynamicSection"> hx-target="#credentialUpdateDynamicSection">
Add Passkey Add Passkey
</button> </button>
<button type="button" <button type="button"
class="btn btn-primary dropdown-toggle dropdown-toggle-split" class="btn btn-secondary dropdown-toggle dropdown-toggle-split"
data-bs-toggle="dropdown" aria-expanded="false"> data-bs-toggle="dropdown" aria-expanded="false">
<span class="visually-hidden">Toggle Dropdown</span> <span class="visually-hidden">Toggle Dropdown</span>
</button> </button>
@ -112,9 +112,38 @@
(% let primary_state = primary_state %) (% let primary_state = primary_state %)
(% include "credentials_update_primary.html" %) (% include "credentials_update_primary.html" %)
(% match unixcred_state %)
(% when CUCredState::Modifiable %)
<hr class="my-4" /> <hr class="my-4" />
<div id="cred-update-commit-bar" class="toast" role="alert" <h4>UNIX Password</h4>
aria-live="assertive" aria-atomic="true"> <p>This password is used when authenticating to a UNIX-like system</p>
<button type="button" class="btn btn-secondary"
hx-post="/ui/reset/set_unixcred"
hx-target="#credentialUpdateDynamicSection">
Set UNIX Password
</button>
(% match unixcred %)
(% when Some(CredentialDetail { uuid, type_: kanidm_proto::internal::CredentialDetailType::Password }) %)
<button type="button" class="btn btn-outline-danger"
hx-post="/ui/api/delete_unixcred"
hx-target="#credentialUpdateDynamicSection">
Delete UNIX Password
</button>
(% when Some(CredentialDetail { uuid, type_: kanidm_proto::internal::CredentialDetailType::GeneratedPassword }) %)
(% when Some(CredentialDetail { uuid, type_: kanidm_proto::internal::CredentialDetailType::Passkey(_) }) %)
(% when Some(CredentialDetail { uuid, type_: kanidm_proto::internal::CredentialDetailType::PasswordMfa(_totp_set, _security_key_labels, _backup_codes_remaining)}) %)
(% when None %)
(% endmatch %)
<!-- (% if matches!(primary_state, CUCredState::Modifiable) %)
(% endif %) -->
(% when CUCredState::DeleteOnly %)
(% when CUCredState::AccessDeny %)
(% when CUCredState::PolicyDeny %)
(% endmatch %)
<hr class="my-4" />
<div id="cred-update-commit-bar" class="toast">
<div class="toast-body"> <div class="toast-body">
<span class="d-flex align-items-center"> <span class="d-flex align-items-center">
<div> <div>

View file

@ -8,11 +8,12 @@
<a target="_blank" href="https://support.apple.com/guide/iphone/use-passkeys-to-sign-in-to-apps-and-websites-iphf538ea8d0/ios">iOS</a> <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. have built-in support for passkeys.
</p> </p>
<ul class="list-group">
(% for passkey in passkeys %) (% for passkey in passkeys %)
<div class="row mb-3"> <div class="list-group-item d-flex justify-content-between align-items-center">
<div class="col d-flex align-items-center"><span>(( passkey.tag ))</span></div> <div>(( passkey.tag ))</div>
<div class="col d-flex justify-content-end"> <div>
<button type="button" class="btn btn-danger btn-sml" id="(( passkey.tag ))" <button type="button" class="btn btn-danger btn-tiny" id="(( passkey.tag ))"
hx-target="#credentialUpdateDynamicSection" hx-target="#credentialUpdateDynamicSection"
hx-confirm="Are you sure you want to delete passkey (( passkey.tag )) ?" hx-confirm="Are you sure you want to delete passkey (( passkey.tag )) ?"
hx-post="/ui/api/remove_passkey" hx-vals='{"uuid": "(( passkey.uuid ))"}'> hx-post="/ui/api/remove_passkey" hx-vals='{"uuid": "(( passkey.uuid ))"}'>
@ -20,4 +21,5 @@
</button> </button>
</div> </div>
</div> </div>
(% endfor %) (% endfor %)
</ul>

View file

@ -17,77 +17,108 @@
</p> </p>
(% if matches!(primary_state, CUCredState::Modifiable) %) (% if matches!(primary_state, CUCredState::Modifiable) %)
(% match primary %) <div class="d-flex flex-column row-gap-4">
(% when Some(CredentialDetail { uuid, type_: kanidm_proto::internal::CredentialDetailType::Password }) %) (% match primary %)
<h6><b>Password</b></h6> (% when Some(CredentialDetail { uuid, type_: kanidm_proto::internal::CredentialDetailType::Password }) %)
<p> <div class="d-flex justify-content-between">
<button type="button" class="btn btn-primary" hx-post="/ui/reset/change_password"> <div>
Change Password <h6><b>Password</b></h6>
</button> </div>
</p> <div class="flex-shrink-0 ps-3">
<h6><b>Time-based One Time Password (TOTP)</b></h6> <button type="button" class="btn btn-sm btn-secondary" hx-post="/ui/reset/change_password">
<p>TOTPs are 6 digit codes generated on-demand as a second authentication factor.</p> Change Password
<p> </button>
<button type="button" class="btn btn-primary" hx-post="/ui/reset/add_totp"> </div>
Add TOTP </div>
</button> <div class="d-flex justify-content-between">
</p> <div>
<br/> <h6><b>Time-based One Time Password (TOTP)</b></h6>
<p> <p>TOTPs are 6 digit codes generated on-demand as a second authentication factor.</p>
<button type="button" class="btn btn-danger" hx-post="/ui/api/delete_alt_creds" hx-confirm="Delete your Password and any associated MFA?\nNote: this will not remove Passkeys."> </div>
Delete Alternative Credentials <div class="flex-shrink-0 ps-3">
</button> <button type="button" class="btn btn-sm btn-secondary" hx-post="/ui/reset/add_totp">
</p> Add TOTP
(% when Some(CredentialDetail { uuid, type_: kanidm_proto::internal::CredentialDetailType::PasswordMfa(totp_set, _security_key_labels, _backup_codes_remaining)}) %) </button>
<h6><b>Password</b></h6> </div>
<p> </div>
<button type="button" class="btn btn-primary" hx-post="/ui/reset/change_password"> <div>
Change Password <button type="button" class="btn btn-outline-danger" hx-post="/ui/api/delete_alt_creds" hx-confirm="Delete your Password and any associated MFA?\nNote: this will not remove Passkeys.">
</button> Delete Alternative Credentials
</p> </button>
<h6><b>Time-based One Time Password (TOTP)</b></h6> </div>
<p>TOTPs are 6 digit codes generated on-demand as a second authentication factor.</p> (% when Some(CredentialDetail { uuid, type_: kanidm_proto::internal::CredentialDetailType::PasswordMfa(totp_set, _security_key_labels, _backup_codes_remaining)}) %)
(% for totp in totp_set %) <div class="d-flex justify-content-between">
<button type="button" class="btn btn-warning mb-2" hx-post="/ui/api/remove_totp" hx-vals='{"name": "(( totp ))"}'> <div>
Remove totp (( totp )) <h6><b>Password</b></h6>
</button> </div>
(% endfor %) <div class="flex-shrink-0 ps-3">
<button type="button" class="btn btn-sm btn-secondary" hx-post="/ui/reset/change_password">
<p> Change Password
<button type="button" class="btn btn-primary" hx-post="/ui/reset/add_totp"> </button>
Add TOTP </div>
</button> </div>
</p> <div>
<br/> <div class="d-flex justify-content-between">
<p> <div>
<button type="button" class="btn btn-danger" hx-post="/ui/api/delete_alt_creds" hx-confirm="Delete your Password and any associated MFA? <h6><b>Time-based One Time Password (TOTP)</b></h6>
Note: this will not remove Passkeys."> <p>TOTPs are 6 digit codes generated on-demand as a second authentication factor.</p>
Delete Alternative Credentials </div>
</button> <div class="flex-shrink-0 ps-3">
</p> <button type="button" class="btn btn-sm btn-secondary" hx-post="/ui/reset/add_totp">
(% when Some(CredentialDetail { uuid, type_: kanidm_proto::internal::CredentialDetailType::GeneratedPassword }) %) Add TOTP
<h6><b>Password</b></h6> </button>
<p>In order to set up alternative authentication methods, you must delete the generated password.</p> </div>
<button type="button" class="btn btn-danger" hx-post="/ui/api/delete_alt_creds" > </div>
Delete Generated Password <p>Registered authenticators:</p>
</button> <ul class="list-group">
(% when Some(CredentialDetail { uuid, type_: kanidm_proto::internal::CredentialDetailType::Passkey(_) }) %) (% for totp in totp_set %)
<p>Webauthn Only - Will migrate to passkeys in a future update</p> <li class="list-group-item">
<button type="button" class="btn btn-danger" hx-post="/ui/api/delete_alt_creds" hx-confirm="Delete your Password and any associated MFA? <div class="d-flex justify-content-between">
Note: this will not remove Passkeys."> <div>(( totp ))</div>
Delete Alternative Credentials <button type="button" class="btn btn-tiny btn-danger" hx-post="/ui/api/remove_totp" hx-vals='{"name": "(( totp ))"}'>
</button> Remove
(% when None %) </button>
<button type="button" class="btn btn-warning" hx-post="/ui/reset/add_password"> </div>
Add Password </li>
</button> (% endfor %)
(% endmatch %) </ul>
</div>
<div>
<button type="button" class="btn btn-outline-danger" hx-post="/ui/api/delete_alt_creds" hx-confirm="Delete your Password and any associated MFA?
Note: this will not remove Passkeys.">
Delete Alternative Credentials
</button>
</div>
(% when Some(CredentialDetail { uuid, type_: kanidm_proto::internal::CredentialDetailType::GeneratedPassword }) %)
<div>
<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-outline-danger" hx-post="/ui/api/delete_alt_creds" >
Delete Generated Password
</button>
</div>
(% when Some(CredentialDetail { uuid, type_: kanidm_proto::internal::CredentialDetailType::Passkey(_) }) %)
<div>
<p>Webauthn Only - Will migrate to passkeys in a future update</p>
<button type="button" class="btn btn-outline-danger" hx-post="/ui/api/delete_alt_creds" hx-confirm="Delete your Password and any associated MFA?
Note: this will not remove Passkeys.">
Delete Alternative Credentials
</button>
</div>
(% when None %)
<div>
<button type="button" class="btn btn-warning" hx-post="/ui/reset/add_password">
Add Password
</button>
</div>
(% endmatch %)
</div>
(% else if matches!(primary_state, CUCredState::DeleteOnly) %) (% else if matches!(primary_state, CUCredState::DeleteOnly) %)
<p> <div>
<button type="button" class="btn btn-warning" hx-post="/ui/api/delete_alt_creds" hx-confirm="Delete your Password and any associated MFA? <button type="button" class="btn btn-warning" hx-post="/ui/api/delete_alt_creds" hx-confirm="Delete your Password and any associated MFA?
Note: this will not remove Passkeys."> Note: this will not remove Passkeys.">
Delete Legacy Credentials Delete Legacy Credentials
</button> </button>
</p> </div>
(% endif %) (% endif %)
</div> </div>

View file

@ -21,26 +21,27 @@
value="(( username ))" value="(( username ))"
required=true required=true
/> />
<input
class="input-hidden"
id="password"
name="password"
type="password"
autocomplete="current-password"
value=""
/>
<input
class="input-hidden"
id="totp"
name="totp"
type="number"
autocomplete="one-time-code"
value=""
/>
</div> </div>
<!-- BEGIN: to work better with password managers -->
<input
class="d-none"
id="password"
name="password"
type="password"
autocomplete="current-password"
value=""
/>
<input
class="d-none"
id="totp"
name="totp"
type="number"
autocomplete="one-time-code"
value=""
/>
<!-- END -->
<div class="mb-3 form-check form-switch"> <div class="mb-3 form-check form-switch">
<input <input
type="checkbox" type="checkbox"

View file

@ -6,24 +6,22 @@
(% endblock %) (% endblock %)
(% block body %) (% block body %)
<main id="main" class="flex-shrink-0 form-signin"> <main id="main" class="form-signin m-auto align-items-center d-flex flex-column">
<center> (% if display_ctx.domain_info.image().is_some() %)
(% if display_ctx.domain_info.image().is_some() %) <img src="/ui/images/domain"
<img src="/ui/images/domain" alt="Kanidm" class="kanidm_logo" />
alt="Kanidm" class="kanidm_logo" /> (% else %)
(% else %) <img
<img src="/pkg/img/logo-square.svg?v=((crate::https::cache_buster::get_cache_buster_key()))"
src="/pkg/img/logo-square.svg?v=((crate::https::cache_buster::get_cache_buster_key()))" alt="Kanidm" class="kanidm_logo" />
alt="Kanidm" class="kanidm_logo" /> (% endif %)
(% endif %) <h3>Kanidm</h3>
<h3>Kanidm</h3> (% if let Some(reauth) = display_ctx.reauth %)
(% if let Some(reauth) = display_ctx.reauth %) <div class="alert alert-info" role="alert">
<h5>Reauthenticating as (( reauth.username )) to access (( reauth.purpose Reauthenticating as (( reauth.username )) to access (( reauth.purpose ))
))</h5> </div>
(% endif %) (% endif %)
</center> <div>
<div id="login-form-container" class="container">
(% block logincontainer %) (% block logincontainer %)
(% endblock %) (% endblock %)
</div> </div>

View file

@ -12,22 +12,22 @@
src="/pkg/pkhtml.js?v=((crate::https::cache_buster::get_cache_buster_key()))" src="/pkg/pkhtml.js?v=((crate::https::cache_buster::get_cache_buster_key()))"
defer></script> defer></script>
<div class="identity-verification-container"> <div class="justify-content-center">
(% if passkey %) (% if passkey %)
<form id="cred-form" action="/ui/login/passkey" method="POST"> <form id="cred-form" action="/ui/login/passkey" method="POST">
<input hidden="hidden" name="cred" id="cred"> <input hidden="hidden" name="cred" id="cred">
<button hx-disable type="button" class="btn btn-dark" <button hx-disable type="button" class="btn btn-dark"
id="start-passkey-button">Use Passkey</button> id="start-passkey-button">Use Passkey</button>
</form> </form>
(% else %) (% else %)
<form id="cred-form" action="/ui/login/seckey" method="POST"> <form id="cred-form" action="/ui/login/seckey" method="POST">
<input hidden="hidden" name="cred" id="cred"> <input hidden="hidden" name="cred" id="cred">
<button type="button" class="btn btn-dark" id="start-seckey-button" <button type="button" class="btn btn-dark" id="start-seckey-button"
>Use Security Key</button> >Use Security Key</button>
</form> </form>
(% endif %) (% endif %)
</div> </div>
(% endblock %) (% endblock %)

View file

@ -1,33 +1,42 @@
<nav class="(( crate::https::ui::CSS_NAVBAR_NAV ))"> <nav class="(( crate::https::ui::CSS_NAVBAR_NAV ))">
<div class="container-fluid"> <div class="container-lg">
<a class="(( crate::https::ui::CSS_NAVBAR_BRAND ))" <a class="(( crate::https::ui::CSS_NAVBAR_BRAND ))" href="/ui/apps">
href="/ui/apps">Kanidm</a> (% if navbar_ctx.domain_info.image().is_some() %)
<img src="/ui/images/domain"
alt="Kanidm" width="auto" height="40" class="navbar-toggler-img" />
(% else %)
<img
src="/pkg/img/logo.svg?v=((crate::https::cache_buster::get_cache_buster_key()))"
alt="Kanidm" width="auto" height="40" class="navbar-toggler-img" />
(% endif %)
<div class="ps-2">Kanidm</div>
</a>
<!-- this shows a button on mobile devices to open the menu--> <!-- this shows a button on mobile devices to open the menu-->
<button class="navbar-toggler bg-white" type="button" <button class="navbar-toggler" type="button"
data-bs-toggle="collapse" data-bs-target="#navbarCollapse" data-bs-toggle="collapse" data-bs-target="#navbarCollapse"
aria-controls="navbarCollapse" aria-expanded="false" aria-controls="navbarCollapse" aria-expanded="false"
aria-label="Toggle navigation"> aria-label="Toggle navigation">
<img <span class="navbar-toggler-icon"></span>
src="/pkg/img/logo-square.svg?v=((crate::https::cache_buster::get_cache_buster_key()))"
alt="Toggle navigation" class="navbar-toggler-img" />
</button> </button>
<div class="collapse navbar-collapse" id="navbarCollapse"> <div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="(( crate::https::ui::CSS_NAVBAR_LINKS_UL ))"> <ul class="(( crate::https::ui::CSS_NAVBAR_LINKS_UL ))">
<li class="mb-1"> <li>
<a class="nav-link" href=((Urls::Apps))> <a class="nav-link" href=((Urls::Apps))>
<span data-feather="file"></span>Apps</a> <span data-feather="file"></span>Apps</a>
</li> </li>
<li class="mb-1"> <li>
<a class="nav-link" href=((Urls::Profile))> <a class="nav-link" href=((Urls::Profile))>
<span data-feather="file"></span>Profile</a> <span data-feather="file"></span>Profile</a>
</li> </li>
<li class="mb-1"> </ul>
<ul class="(( crate::https::ui::CSS_NAVBAR_LINKS_UL )) ms-md-auto">
<li>
<a class="nav-link" href="#" data-bs-toggle="modal" <a class="nav-link" href="#" data-bs-toggle="modal"
data-bs-target="#signoutModal">Sign out</a> data-bs-target="#signoutModal">Sign out</a>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
</nav> </nav>

View file

@ -6,7 +6,7 @@
(% endblock %) (% endblock %)
(% block body %) (% block body %)
<main id="main" class="flex-shrink-0 form-signin"> <main id="main" class="flex-shrink-0 form-signin m-auto">
<h2 class="h3 mb-3 fw-normal">Consent to Proceed to (( client_name ))</h2> <h2 class="h3 mb-3 fw-normal">Consent to Proceed to (( client_name ))</h2>
(% if pii_scopes.is_empty() %) (% if pii_scopes.is_empty() %)
<div> <div>

View file

@ -1,29 +1,31 @@
(% macro side_menu_item(label, href, menu_item, icon_name) %) (% macro side_menu_item(label, href, menu_item, icon_name) %)
<a hx-select="main" hx-target="main" hx-swap="outerHTML show:false" <li>
href="(( href ))" <a hx-select="main" hx-target="main" hx-swap="outerHTML show:false"
class="list-group-item list-group-item-action d-flex (% if menu_active_item == menu_item %) active(% endif %)"> href="(( href ))"
<img class="me-3" class="side-menu-item d-flex rounded link-dark(% if menu_active_item == menu_item %) active(% endif %)">
src="/pkg/img/icons/(( icon_name )).svg?v=((crate::https::cache_buster::get_cache_buster_key()))" <div class="icon-container align-items-center justify-content-center d-flex me-2">
alt>(( label )) <img class="text-body-secondary"
</a> src="/pkg/img/icons/(( icon_name )).svg?v=((crate::https::cache_buster::get_cache_buster_key()))"
alt>
</div>
<div>(( label ))</div>
</a>
</li>
(% endmacro %) (% endmacro %)
<main id="main" class="container-xxl pb-5"> <main id="main" class="container-lg pb-5">
<div class="d-flex flex-sm-row flex-column"> <div class="d-flex flex-sm-row flex-column">
<div class="list-group side-menu flex-shrink-0"> <ul class="side-menu list-unstyled flex-shrink-0 row-gap-1 d-flex flex-column">
(% call side_menu_item("Profile", (Urls::Profile), (% call side_menu_item("Profile", (Urls::Profile),
ProfileMenuItems::UserProfile, "person") %) ProfileMenuItems::UserProfile, "person") %)
(% if posix_enabled %)
(% call side_menu_item("UNIX Password", (Urls::UpdateCredentials),
ProfileMenuItems::UnixPassword, "building-lock") %)
(% endif %)
(% call side_menu_item("Credentials", (Urls::UpdateCredentials), (% call side_menu_item("Credentials", (Urls::UpdateCredentials),
ProfileMenuItems::Credentials, "shield-lock") %) ProfileMenuItems::Credentials, "shield-lock") %)
</div> </ul>
<div id="settings-window" class="flex-grow-1 ps-sm-4 pt-sm-0 pt-4"> <div id="settings-window" class="flex-grow-1 ps-sm-4 ps-md-5 pt-sm-0 pt-4">
<div class="(( crate::https::ui::CSS_PAGE_HEADER ))"> <div>
<h2>(% block selected_setting_group %)(% endblock %)</h2> <h2>(% block selected_setting_group %)(% endblock %)</h2>
</div> </div>
<hr />
(% block settings_window %) (% block settings_window %)
(% endblock %) (% endblock %)

View file

@ -8,23 +8,23 @@ Profile
<form> <form>
<div class="mb-2 row"> <div class="mb-2 row">
<label for="profileUserName" class="col-12 col-md-3 col-lg-2 col-form-label">User name</label> <label for="profileUserName" class="col-12 col-md-3 col-xl-2 col-form-label">User name</label>
<div class="col-12 col-md-6 col-lg-4"> <div class="col-12 col-md-6 col-lg-5">
<input type="text" readonly class="form-control-plaintext" id="profileUserName" value="(( account_name ))"> <input type="text" readonly class="form-control-plaintext" id="profileUserName" value="(( account_name ))">
</div> </div>
</div> </div>
<div class="mb-2 row"> <div class="mb-2 row">
<label for="profileDisplayName" class="col-12 col-md-3 col-lg-2 col-form-label">Display name</label> <label for="profileDisplayName" class="col-12 col-md-3 col-xl-2 col-form-label">Display name</label>
<div class="col-12 col-md-6 col-lg-4"> <div class="col-12 col-md-6 col-lg-5">
<input type="text" class="form-control-plaintext" id="profileDisplayName" value="(( display_name ))" disabled> <input type="text" class="form-control-plaintext" id="profileDisplayName" value="(( display_name ))" disabled>
</div> </div>
</div> </div>
<div class="mb-2 row"> <div class="mb-2 row">
<label for="profileEmail" class="col-12 col-md-3 col-lg-2 col-form-label">Email</label> <label for="profileEmail" class="col-12 col-md-3 col-xl-2 col-form-label">Email</label>
<div class="col-12 col-md-6 col-lg-4"> <div class="col-12 col-md-6 col-lg-5">
<input type="email" disabled class="form-control-plaintext" id="profileEmail" value="(( email.clone().unwrap_or("None configured".to_string())))" > <input type="email" disabled class="form-control-plaintext" id="profileEmail" value="(( email.clone().unwrap_or("None configured".to_string())))">
</div> </div>
</div> </div>

View file

@ -1,5 +1,6 @@
use crate::entry::EntryInitNew; use crate::entry::EntryInitNew;
use crate::prelude::*; use crate::prelude::*;
use crate::value::CredentialType;
use kanidm_proto::internal::{Filter, OperationError, UiHint}; use kanidm_proto::internal::{Filter, OperationError, UiHint};
@ -326,6 +327,8 @@ lazy_static! {
(Attribute::Class, EntryClass::AccountPolicy.to_value()), (Attribute::Class, EntryClass::AccountPolicy.to_value()),
// Enforce this is a system protected object // Enforce this is a system protected object
(Attribute::Class, EntryClass::System.to_value()), (Attribute::Class, EntryClass::System.to_value()),
// MFA By Default
(Attribute::CredentialTypeMinimum, CredentialType::Mfa.into()),
], ],
..Default::default() ..Default::default()
}; };