mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
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:
parent
151a9ad90f
commit
bc55313d87
|
@ -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()
|
||||||
|
|
|
@ -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}"
|
||||||
|
|
|
@ -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 },
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -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))
|
||||||
|
|
5
server/core/src/https/views/navbar.rs
Normal file
5
server/core/src/https/views/navbar.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
use crate::https::extractors::DomainInfoRead;
|
||||||
|
|
||||||
|
pub struct NavbarCtx {
|
||||||
|
pub domain_info: DomainInfoRead,
|
||||||
|
}
|
|
@ -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,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
1113
server/core/static/img/logo.svg
Normal file
1113
server/core/static/img/logo.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 839 KiB |
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
@ -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 %)
|
||||||
|
|
|
@ -5,3 +5,4 @@
|
||||||
(% block main %)(% endblock %)
|
(% block main %)(% endblock %)
|
||||||
(% include "signout_modal.html" %)
|
(% include "signout_modal.html" %)
|
||||||
(% endblock %)
|
(% endblock %)
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 %)
|
||||||
|
|
|
@ -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()))"
|
||||||
|
|
|
@ -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 %)
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 %)
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 %)
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue