mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
yale's rabbit-hole-chasing-htmx-fixing-megapatch (#3135)
This commit is contained in:
parent
31420c3ff9
commit
bbe9ad1a06
12
Cargo.lock
generated
12
Cargo.lock
generated
|
@ -160,6 +160,17 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama_axum"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a41603f7cdbf5ac4af60760f17253eb6adf6ec5b6f14a7ed830cf687d375f163"
|
||||
dependencies = [
|
||||
"askama",
|
||||
"axum-core 0.4.5",
|
||||
"http 1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama_derive"
|
||||
version = "0.12.5"
|
||||
|
@ -3434,6 +3445,7 @@ name = "kanidmd_core"
|
|||
version = "1.4.0-dev"
|
||||
dependencies = [
|
||||
"askama",
|
||||
"askama_axum",
|
||||
"async-trait",
|
||||
"axum 0.7.7",
|
||||
"axum-auth",
|
||||
|
|
|
@ -141,7 +141,8 @@ sketching = { path = "./libs/sketching", version = "=1.4.0-dev" }
|
|||
|
||||
anyhow = { version = "1.0.90" }
|
||||
argon2 = { version = "0.5.3", features = ["alloc"] }
|
||||
askama = { version = "0.12.1", features = ["serde"] }
|
||||
askama = { version = "0.12.1", features = ["serde", "with-axum"] }
|
||||
askama_axum = { version = "0.4.0" }
|
||||
async-trait = "^0.1.83"
|
||||
axum = { version = "0.7.7", features = [
|
||||
"form",
|
||||
|
@ -248,7 +249,9 @@ reqwest = { version = "0.12.8", default-features = false, features = [
|
|||
] }
|
||||
rpassword = "^7.3.1"
|
||||
rusqlite = { version = "^0.28.0", features = ["array", "bundled"] }
|
||||
rustls = { version = "0.23.13", default-features = false, features = ["aws_lc_rs"] }
|
||||
rustls = { version = "0.23.13", default-features = false, features = [
|
||||
"aws_lc_rs",
|
||||
] }
|
||||
|
||||
sd-notify = "^0.4.3"
|
||||
selinux = "^0.4.6"
|
||||
|
|
5
Makefile
5
Makefile
|
@ -39,6 +39,11 @@ run: ## Run the test/dev server
|
|||
run:
|
||||
cd server/daemon && ./run_insecure_dev_server.sh
|
||||
|
||||
.PHONY: run_htmx
|
||||
run_htmx: ## Run in HTMX mode
|
||||
run_htmx:
|
||||
cd server/daemon && KANI_CARGO_OPTS="--features kanidmd_core/ui_htmx" ./run_insecure_dev_server.sh
|
||||
|
||||
.PHONY: buildx/kanidmd
|
||||
buildx/kanidmd: ## Build multiarch kanidm server images and push to docker hub
|
||||
buildx/kanidmd:
|
||||
|
|
|
@ -21,6 +21,7 @@ ui_htmx = []
|
|||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
askama = { workspace = true }
|
||||
askama_axum = { workspace = true }
|
||||
axum = { workspace = true }
|
||||
axum-htmx = { workspace = true }
|
||||
axum-auth = "0.7.0"
|
||||
|
|
|
@ -9,7 +9,7 @@ use axum_htmx::HxPushUrl;
|
|||
|
||||
use kanidm_proto::internal::AppLink;
|
||||
|
||||
use super::HtmlTemplate;
|
||||
use super::constants::Urls;
|
||||
use crate::https::views::errors::HtmxError;
|
||||
use crate::https::{extractors::VerifiedClientInformation, middleware::KOpId, ServerState};
|
||||
|
||||
|
@ -40,13 +40,12 @@ pub(crate) async fn view_apps_get(
|
|||
.await
|
||||
.map_err(|old| HtmxError::new(&kopid, old))?;
|
||||
|
||||
let apps_partial = AppsPartialView { apps: app_links };
|
||||
|
||||
Ok({
|
||||
let apps_view = AppsView { apps_partial };
|
||||
(
|
||||
HxPushUrl(Uri::from_static("/ui/apps")),
|
||||
HtmlTemplate(apps_view).into_response(),
|
||||
HxPushUrl(Uri::from_static(Urls::Apps.as_ref())),
|
||||
AppsView {
|
||||
apps_partial: AppsPartialView { apps: app_links },
|
||||
},
|
||||
)
|
||||
.into_response()
|
||||
})
|
||||
|
|
|
@ -4,7 +4,47 @@ use serde::{Deserialize, Serialize};
|
|||
#[serde(rename_all = "snake_case")]
|
||||
pub(crate) enum ProfileMenuItems {
|
||||
UserProfile,
|
||||
SshKeys,
|
||||
Credentials,
|
||||
UnixPassword,
|
||||
}
|
||||
|
||||
pub(crate) enum UiMessage {
|
||||
UnlockEdit,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for UiMessage {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
UiMessage::UnlockEdit => write!(f, "Unlock Edit 🔒"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) enum Urls {
|
||||
Apps,
|
||||
CredReset,
|
||||
Profile,
|
||||
UpdateCredentials,
|
||||
Oauth2Resume,
|
||||
Login,
|
||||
}
|
||||
|
||||
impl AsRef<str> for Urls {
|
||||
fn as_ref(&self) -> &str {
|
||||
match self {
|
||||
Self::Apps => "/ui/apps",
|
||||
Self::CredReset => "/ui/reset",
|
||||
Self::Profile => "/ui/profile",
|
||||
Self::UpdateCredentials => "/ui/update_credentials",
|
||||
Self::Oauth2Resume => "/ui/oauth2/resume",
|
||||
Self::Login => "/ui/login",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Urls {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.as_ref())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ use uuid::Uuid;
|
|||
use kanidm_proto::internal::OperationError;
|
||||
|
||||
use crate::https::middleware::KOpId;
|
||||
use crate::https::views::{HtmlTemplate, UnrecoverableErrorView};
|
||||
use crate::https::views::UnrecoverableErrorView;
|
||||
// #[derive(Template)]
|
||||
// #[template(path = "recoverable_error_partial.html")]
|
||||
// struct ErrorPartialView {
|
||||
|
@ -55,10 +55,10 @@ impl IntoResponse for HtmxError {
|
|||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
HxRetarget("body".to_string()),
|
||||
HxReswap(SwapOption::OuterHtml),
|
||||
HtmlTemplate(UnrecoverableErrorView {
|
||||
UnrecoverableErrorView {
|
||||
err_code: inner,
|
||||
operation_id: kopid,
|
||||
}),
|
||||
},
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use super::{cookies, empty_string_as_none, HtmlTemplate, UnrecoverableErrorView};
|
||||
use super::constants::Urls;
|
||||
use super::{cookies, empty_string_as_none, UnrecoverableErrorView};
|
||||
use crate::https::views::errors::HtmxError;
|
||||
use crate::https::{
|
||||
extractors::{DomainInfo, DomainInfoRead, VerifiedClientInformation},
|
||||
|
@ -53,7 +54,19 @@ pub enum ReauthPurpose {
|
|||
impl fmt::Display for ReauthPurpose {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ReauthPurpose::ProfileSettings => write!(f, "Profile and Settings"),
|
||||
Self::ProfileSettings => write!(f, "Profile and Settings"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum LoginError {
|
||||
InvalidUsername,
|
||||
}
|
||||
|
||||
impl fmt::Display for LoginError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::InvalidUsername => write!(f, "Invalid username"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -67,6 +80,7 @@ pub struct LoginDisplayCtx {
|
|||
pub domain_info: DomainInfoRead,
|
||||
// We only need this on the first re-auth screen to indicate what we are doing
|
||||
pub reauth: Option<Reauth>,
|
||||
pub error: Option<LoginError>,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
|
@ -146,13 +160,13 @@ pub async fn view_logout_get(
|
|||
.handle_logout(client_auth_info, kopid.eventid)
|
||||
.await
|
||||
{
|
||||
HtmlTemplate(UnrecoverableErrorView {
|
||||
UnrecoverableErrorView {
|
||||
err_code,
|
||||
operation_id: kopid.eventid,
|
||||
})
|
||||
}
|
||||
.into_response()
|
||||
} else {
|
||||
let response = Redirect::to("/ui/login").into_response();
|
||||
let response = Redirect::to(Urls::Login.as_ref()).into_response();
|
||||
|
||||
jar = cookies::destroy(jar, COOKIE_BEARER_TOKEN);
|
||||
|
||||
|
@ -167,13 +181,13 @@ pub async fn view_reauth_get(
|
|||
jar: CookieJar,
|
||||
return_location: &str,
|
||||
display_ctx: LoginDisplayCtx,
|
||||
) -> axum::response::Result<Response> {
|
||||
) -> Response {
|
||||
let session_valid_result = state
|
||||
.qe_r_ref
|
||||
.handle_auth_valid(client_auth_info.clone(), kopid.eventid)
|
||||
.await;
|
||||
|
||||
let res = match session_valid_result {
|
||||
match session_valid_result {
|
||||
Ok(()) => {
|
||||
let inter = state
|
||||
.qe_r_ref
|
||||
|
@ -209,18 +223,18 @@ pub async fn view_reauth_get(
|
|||
{
|
||||
Ok(r) => r,
|
||||
// Okay, these errors are actually REALLY bad.
|
||||
Err(err_code) => HtmlTemplate(UnrecoverableErrorView {
|
||||
Err(err_code) => UnrecoverableErrorView {
|
||||
err_code,
|
||||
operation_id: kopid.eventid,
|
||||
})
|
||||
}
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
// Probably needs to be way nicer on login, especially something like no matching users ...
|
||||
Err(err_code) => HtmlTemplate(UnrecoverableErrorView {
|
||||
Err(err_code) => UnrecoverableErrorView {
|
||||
err_code,
|
||||
operation_id: kopid.eventid,
|
||||
})
|
||||
}
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
@ -233,21 +247,19 @@ pub async fn view_reauth_get(
|
|||
|
||||
let remember_me = !username.is_empty();
|
||||
|
||||
HtmlTemplate(LoginView {
|
||||
LoginView {
|
||||
display_ctx,
|
||||
username,
|
||||
remember_me,
|
||||
})
|
||||
}
|
||||
.into_response()
|
||||
}
|
||||
Err(err_code) => HtmlTemplate(UnrecoverableErrorView {
|
||||
Err(err_code) => UnrecoverableErrorView {
|
||||
err_code,
|
||||
operation_id: kopid.eventid,
|
||||
})
|
||||
}
|
||||
.into_response(),
|
||||
};
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn view_index_get(
|
||||
|
@ -266,7 +278,7 @@ pub async fn view_index_get(
|
|||
match session_valid_result {
|
||||
Ok(()) => {
|
||||
// Send the user to the landing.
|
||||
Redirect::to("/ui/apps").into_response()
|
||||
Redirect::to(Urls::Apps.as_ref()).into_response()
|
||||
}
|
||||
Err(OperationError::NotAuthenticated) | Err(OperationError::SessionExpired) => {
|
||||
// cookie jar with remember me.
|
||||
|
@ -280,19 +292,20 @@ pub async fn view_index_get(
|
|||
let display_ctx = LoginDisplayCtx {
|
||||
domain_info,
|
||||
reauth: None,
|
||||
error: None,
|
||||
};
|
||||
|
||||
HtmlTemplate(LoginView {
|
||||
LoginView {
|
||||
display_ctx,
|
||||
username,
|
||||
remember_me,
|
||||
})
|
||||
}
|
||||
.into_response()
|
||||
}
|
||||
Err(err_code) => HtmlTemplate(UnrecoverableErrorView {
|
||||
Err(err_code) => UnrecoverableErrorView {
|
||||
err_code,
|
||||
operation_id: kopid.eventid,
|
||||
})
|
||||
}
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
@ -346,16 +359,17 @@ pub async fn view_login_begin_post(
|
|||
|
||||
let session_context = SessionContext {
|
||||
id: None,
|
||||
username,
|
||||
username: username.clone(),
|
||||
password,
|
||||
totp,
|
||||
remember_me,
|
||||
after_auth_loc: None,
|
||||
};
|
||||
|
||||
let display_ctx = LoginDisplayCtx {
|
||||
let mut display_ctx = LoginDisplayCtx {
|
||||
domain_info,
|
||||
reauth: None,
|
||||
error: None,
|
||||
};
|
||||
|
||||
// Now process the response if ok.
|
||||
|
@ -374,19 +388,30 @@ pub async fn view_login_begin_post(
|
|||
{
|
||||
Ok(r) => r,
|
||||
// Okay, these errors are actually REALLY bad.
|
||||
Err(err_code) => HtmlTemplate(UnrecoverableErrorView {
|
||||
Err(err_code) => UnrecoverableErrorView {
|
||||
err_code,
|
||||
operation_id: kopid.eventid,
|
||||
})
|
||||
}
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
// Probably needs to be way nicer on login, especially something like no matching users ...
|
||||
Err(err_code) => HtmlTemplate(UnrecoverableErrorView {
|
||||
Err(err_code) => match err_code {
|
||||
OperationError::NoMatchingEntries => {
|
||||
display_ctx.error = Some(LoginError::InvalidUsername);
|
||||
LoginView {
|
||||
display_ctx,
|
||||
username,
|
||||
remember_me,
|
||||
}
|
||||
.into_response()
|
||||
}
|
||||
_ => UnrecoverableErrorView {
|
||||
err_code,
|
||||
operation_id: kopid.eventid,
|
||||
})
|
||||
}
|
||||
.into_response(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -426,6 +451,7 @@ pub async fn view_login_mech_choose_post(
|
|||
let display_ctx = LoginDisplayCtx {
|
||||
domain_info,
|
||||
reauth: None,
|
||||
error: None,
|
||||
};
|
||||
|
||||
// Now process the response if ok.
|
||||
|
@ -444,18 +470,18 @@ pub async fn view_login_mech_choose_post(
|
|||
{
|
||||
Ok(r) => r,
|
||||
// Okay, these errors are actually REALLY bad.
|
||||
Err(err_code) => HtmlTemplate(UnrecoverableErrorView {
|
||||
Err(err_code) => UnrecoverableErrorView {
|
||||
err_code,
|
||||
operation_id: kopid.eventid,
|
||||
})
|
||||
}
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
// Probably needs to be way nicer on login, especially something like no matching users ...
|
||||
Err(err_code) => HtmlTemplate(UnrecoverableErrorView {
|
||||
Err(err_code) => UnrecoverableErrorView {
|
||||
err_code,
|
||||
operation_id: kopid.eventid,
|
||||
})
|
||||
}
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
@ -474,19 +500,22 @@ pub async fn view_login_totp_post(
|
|||
Form(login_totp_form): Form<LoginTotpForm>,
|
||||
) -> Response {
|
||||
// trim leading and trailing white space.
|
||||
let Ok(totp) = u32::from_str(login_totp_form.totp.trim()) else {
|
||||
let totp = match u32::from_str(login_totp_form.totp.trim()) {
|
||||
Ok(val) => val,
|
||||
Err(_) => {
|
||||
let display_ctx = LoginDisplayCtx {
|
||||
domain_info,
|
||||
reauth: None,
|
||||
error: None,
|
||||
};
|
||||
|
||||
// If not an int, we need to re-render with an error
|
||||
return HtmlTemplate(LoginTotpView {
|
||||
return LoginTotpView {
|
||||
display_ctx,
|
||||
totp: String::default(),
|
||||
errors: LoginTotpError::Syntax,
|
||||
})
|
||||
}
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let auth_cred = AuthCredential::Totp(totp);
|
||||
|
@ -582,6 +611,7 @@ async fn credential_step(
|
|||
let display_ctx = LoginDisplayCtx {
|
||||
domain_info,
|
||||
reauth: None,
|
||||
error: None,
|
||||
};
|
||||
|
||||
let inter = state // This may change in the future ...
|
||||
|
@ -612,18 +642,18 @@ async fn credential_step(
|
|||
{
|
||||
Ok(r) => r,
|
||||
// Okay, these errors are actually REALLY bad.
|
||||
Err(err_code) => HtmlTemplate(UnrecoverableErrorView {
|
||||
Err(err_code) => UnrecoverableErrorView {
|
||||
err_code,
|
||||
operation_id: kopid.eventid,
|
||||
})
|
||||
}
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
// Probably needs to be way nicer on login, especially something like no matching users ...
|
||||
Err(err_code) => HtmlTemplate(UnrecoverableErrorView {
|
||||
Err(err_code) => UnrecoverableErrorView {
|
||||
err_code,
|
||||
operation_id: kopid.eventid,
|
||||
})
|
||||
}
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
@ -668,10 +698,10 @@ async fn view_login_step(
|
|||
// Should never happen.
|
||||
0 => {
|
||||
error!("auth state choose allowed mechs is empty");
|
||||
HtmlTemplate(UnrecoverableErrorView {
|
||||
UnrecoverableErrorView {
|
||||
err_code: OperationError::InvalidState,
|
||||
operation_id: kopid.eventid,
|
||||
})
|
||||
}
|
||||
.into_response()
|
||||
}
|
||||
1 => {
|
||||
|
@ -705,7 +735,7 @@ async fn view_login_step(
|
|||
name: m,
|
||||
})
|
||||
.collect();
|
||||
HtmlTemplate(LoginMechView { display_ctx, mechs }).into_response()
|
||||
LoginMechView { display_ctx, mechs }.into_response()
|
||||
}
|
||||
};
|
||||
// break acts as return in a loop.
|
||||
|
@ -721,48 +751,48 @@ async fn view_login_step(
|
|||
// Shouldn't be possible.
|
||||
0 => {
|
||||
error!("auth state continued allowed mechs is empty");
|
||||
HtmlTemplate(UnrecoverableErrorView {
|
||||
UnrecoverableErrorView {
|
||||
err_code: OperationError::InvalidState,
|
||||
operation_id: kopid.eventid,
|
||||
})
|
||||
}
|
||||
.into_response()
|
||||
}
|
||||
1 => {
|
||||
let auth_allowed = allowed[0].clone();
|
||||
|
||||
match auth_allowed {
|
||||
AuthAllowed::Totp => HtmlTemplate(LoginTotpView {
|
||||
AuthAllowed::Totp => LoginTotpView {
|
||||
display_ctx,
|
||||
totp: session_context.totp.clone().unwrap_or_default(),
|
||||
errors: LoginTotpError::default(),
|
||||
})
|
||||
}
|
||||
.into_response(),
|
||||
AuthAllowed::Password => HtmlTemplate(LoginPasswordView {
|
||||
AuthAllowed::Password => LoginPasswordView {
|
||||
display_ctx,
|
||||
password: session_context.password.clone().unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
.into_response(),
|
||||
AuthAllowed::BackupCode => {
|
||||
HtmlTemplate(LoginBackupCodeView { display_ctx }).into_response()
|
||||
LoginBackupCodeView { display_ctx }.into_response()
|
||||
}
|
||||
AuthAllowed::SecurityKey(chal) => {
|
||||
let chal_json = serde_json::to_string(&chal)
|
||||
.map_err(|_| OperationError::SerdeJsonError)?;
|
||||
HtmlTemplate(LoginWebauthnView {
|
||||
LoginWebauthnView {
|
||||
display_ctx,
|
||||
passkey: false,
|
||||
chal: chal_json,
|
||||
})
|
||||
}
|
||||
.into_response()
|
||||
}
|
||||
AuthAllowed::Passkey(chal) => {
|
||||
let chal_json = serde_json::to_string(&chal)
|
||||
.map_err(|_| OperationError::SerdeJsonError)?;
|
||||
HtmlTemplate(LoginWebauthnView {
|
||||
LoginWebauthnView {
|
||||
display_ctx,
|
||||
passkey: true,
|
||||
chal: chal_json,
|
||||
})
|
||||
}
|
||||
.into_response()
|
||||
}
|
||||
_ => return Err(OperationError::InvalidState),
|
||||
|
@ -808,7 +838,7 @@ async fn view_login_step(
|
|||
&state,
|
||||
COOKIE_USERNAME,
|
||||
session_context.username.clone(),
|
||||
"/ui/login",
|
||||
Urls::Login.as_ref(),
|
||||
);
|
||||
jar.add(username_cookie)
|
||||
} else {
|
||||
|
@ -821,11 +851,11 @@ async fn view_login_step(
|
|||
|
||||
// Now, we need to decided where to go.
|
||||
let res = if jar.get(COOKIE_OAUTH2_REQ).is_some() {
|
||||
Redirect::to("/ui/oauth2/resume").into_response()
|
||||
Redirect::to(Urls::Oauth2Resume.as_ref()).into_response()
|
||||
} else if let Some(auth_loc) = session_context.after_auth_loc {
|
||||
Redirect::to(auth_loc.as_str()).into_response()
|
||||
} else {
|
||||
Redirect::to("/ui/apps").into_response()
|
||||
Redirect::to(Urls::Apps.as_ref()).into_response()
|
||||
};
|
||||
|
||||
break res;
|
||||
|
@ -836,11 +866,11 @@ async fn view_login_step(
|
|||
debug!("🧩 -> AuthState::Denied");
|
||||
jar = jar.remove(Cookie::from(COOKIE_AUTH_SESSION_ID));
|
||||
|
||||
break HtmlTemplate(LoginDeniedView {
|
||||
break LoginDeniedView {
|
||||
display_ctx,
|
||||
reason,
|
||||
operation_id: kopid.eventid,
|
||||
})
|
||||
}
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
|
@ -854,7 +884,12 @@ fn add_session_cookie(
|
|||
jar: CookieJar,
|
||||
session_context: &SessionContext,
|
||||
) -> Result<CookieJar, OperationError> {
|
||||
cookies::make_signed(state, COOKIE_AUTH_SESSION_ID, session_context, "/ui/login")
|
||||
cookies::make_signed(
|
||||
state,
|
||||
COOKIE_AUTH_SESSION_ID,
|
||||
session_context,
|
||||
Urls::Login.as_ref(),
|
||||
)
|
||||
.map(|mut cookie| {
|
||||
// Not needed when redirecting into this site
|
||||
cookie.set_same_site(SameSite::Strict);
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
use askama::Template;
|
||||
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{Html, IntoResponse, Redirect, Response},
|
||||
response::Redirect,
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
|
||||
use axum_htmx::HxRequestGuardLayer;
|
||||
|
||||
use constants::Urls;
|
||||
use kanidmd_lib::prelude::{OperationError, Uuid};
|
||||
|
||||
use crate::https::{
|
||||
|
@ -34,7 +34,10 @@ struct UnrecoverableErrorView {
|
|||
|
||||
pub fn view_router() -> Router<ServerState> {
|
||||
let unguarded_router = Router::new()
|
||||
.route("/", get(|| async { Redirect::permanent("/ui/login") }))
|
||||
.route(
|
||||
"/",
|
||||
get(|| async { Redirect::permanent(Urls::Login.as_ref()) }),
|
||||
)
|
||||
.route("/apps", get(apps::view_apps_get))
|
||||
.route("/reset", get(reset::view_reset_get))
|
||||
.route("/update_credentials", get(reset::view_self_reset_get))
|
||||
|
@ -90,35 +93,13 @@ pub fn view_router() -> Router<ServerState> {
|
|||
.route("/api/remove_passkey", post(reset::remove_passkey))
|
||||
.route("/api/finish_passkey", post(reset::finish_passkey))
|
||||
.route("/api/cancel_mfareg", post(reset::cancel_mfareg))
|
||||
.route("/api/cu_cancel", post(reset::cancel))
|
||||
.route("/api/cu_cancel", post(reset::cancel_cred_update))
|
||||
.route("/api/cu_commit", post(reset::commit))
|
||||
.layer(HxRequestGuardLayer::new("/ui"));
|
||||
|
||||
Router::new().merge(unguarded_router).merge(guarded_router)
|
||||
}
|
||||
|
||||
struct HtmlTemplate<T>(T);
|
||||
|
||||
/// Allows us to convert Askama HTML templates into valid HTML for axum to serve in the response.
|
||||
impl<T> IntoResponse for HtmlTemplate<T>
|
||||
where
|
||||
T: askama::Template,
|
||||
{
|
||||
fn into_response(self) -> Response {
|
||||
// Attempt to render the template with askama
|
||||
match self.0.render() {
|
||||
// If we're able to successfully parse and aggregate the template, serve it
|
||||
Ok(html) => Html(html).into_response(),
|
||||
// If we're not, return an error or some bit of fallback HTML
|
||||
Err(err) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to render template. Error: {}", err),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Serde deserialization decorator to map empty Strings to None,
|
||||
fn empty_string_as_none<'de, D, T>(de: D) -> Result<Option<T>, D::Error>
|
||||
where
|
||||
|
|
|
@ -21,7 +21,8 @@ use axum_extra::extract::cookie::{CookieJar, SameSite};
|
|||
use axum_htmx::HX_REDIRECT;
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::{cookies, HtmlTemplate, UnrecoverableErrorView};
|
||||
use super::constants::Urls;
|
||||
use super::{cookies, UnrecoverableErrorView};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "oauth2_consent_request.html")]
|
||||
|
@ -61,10 +62,10 @@ pub async fn view_resume_get(
|
|||
oauth2_auth_req(state, kopid, client_auth_info, jar, auth_req).await
|
||||
} else {
|
||||
error!("unable to resume session, no auth_req was found in the cookie");
|
||||
HtmlTemplate(UnrecoverableErrorView {
|
||||
UnrecoverableErrorView {
|
||||
err_code: OperationError::InvalidState,
|
||||
operation_id: kopid.eventid,
|
||||
})
|
||||
}
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
|
@ -122,12 +123,12 @@ async fn oauth2_auth_req(
|
|||
consent_token,
|
||||
}) => {
|
||||
// We can just render the form now, the consent token has everything we need.
|
||||
HtmlTemplate(ConsentRequestView {
|
||||
ConsentRequestView {
|
||||
client_name,
|
||||
// scopes,
|
||||
pii_scopes,
|
||||
consent_token,
|
||||
})
|
||||
}
|
||||
.into_response()
|
||||
}
|
||||
Err(Oauth2Error::AuthenticationRequired) => {
|
||||
|
@ -140,19 +141,19 @@ async fn oauth2_auth_req(
|
|||
.ok_or(OperationError::InvalidSessionState);
|
||||
|
||||
match maybe_jar {
|
||||
Ok(jar) => (jar, Redirect::to("/ui/login")).into_response(),
|
||||
Err(err_code) => HtmlTemplate(UnrecoverableErrorView {
|
||||
Ok(jar) => (jar, Redirect::to(Urls::Login.as_ref())).into_response(),
|
||||
Err(err_code) => UnrecoverableErrorView {
|
||||
err_code,
|
||||
operation_id: kopid.eventid,
|
||||
})
|
||||
}
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
Err(Oauth2Error::AccessDenied) => {
|
||||
// If scopes are not available for this account.
|
||||
HtmlTemplate(AccessDeniedView {
|
||||
AccessDeniedView {
|
||||
operation_id: kopid.eventid,
|
||||
})
|
||||
}
|
||||
.into_response()
|
||||
}
|
||||
/*
|
||||
|
@ -172,10 +173,10 @@ async fn oauth2_auth_req(
|
|||
&err_code.to_string()
|
||||
);
|
||||
|
||||
HtmlTemplate(UnrecoverableErrorView {
|
||||
UnrecoverableErrorView {
|
||||
err_code: OperationError::InvalidState,
|
||||
operation_id: kopid.eventid,
|
||||
})
|
||||
}
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
|
@ -231,10 +232,10 @@ pub async fn view_consent_post(
|
|||
&err_code.to_string()
|
||||
);
|
||||
|
||||
HtmlTemplate(UnrecoverableErrorView {
|
||||
UnrecoverableErrorView {
|
||||
err_code: OperationError::InvalidState,
|
||||
operation_id: kopid.eventid,
|
||||
})
|
||||
}
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +1,21 @@
|
|||
use crate::https::errors::WebError;
|
||||
use crate::https::extractors::{DomainInfo, VerifiedClientInformation};
|
||||
use crate::https::middleware::KOpId;
|
||||
use crate::https::views::errors::HtmxError;
|
||||
use crate::https::views::login::{LoginDisplayCtx, Reauth, ReauthPurpose};
|
||||
use crate::https::views::HtmlTemplate;
|
||||
use crate::https::ServerState;
|
||||
use askama::Template;
|
||||
use axum::extract::State;
|
||||
use axum::http::Uri;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::response::Response;
|
||||
use axum::Extension;
|
||||
use axum_extra::extract::cookie::CookieJar;
|
||||
use axum_htmx::{HxPushUrl, HxRequest};
|
||||
use futures_util::TryFutureExt;
|
||||
use kanidm_proto::internal::UserAuthToken;
|
||||
|
||||
use super::constants::ProfileMenuItems;
|
||||
use super::constants::{ProfileMenuItems, UiMessage, Urls};
|
||||
use super::errors::HtmxError;
|
||||
use super::login::{LoginDisplayCtx, Reauth, ReauthPurpose};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "user_settings.html")]
|
||||
struct ProfileView {
|
||||
pub(crate) struct ProfileView {
|
||||
profile_partial: ProfilePartialView,
|
||||
}
|
||||
|
||||
|
@ -29,7 +26,6 @@ struct ProfilePartialView {
|
|||
can_rw: bool,
|
||||
account_name: String,
|
||||
display_name: String,
|
||||
legal_name: String,
|
||||
email: Option<String>,
|
||||
posix_enabled: bool,
|
||||
}
|
||||
|
@ -37,40 +33,26 @@ struct ProfilePartialView {
|
|||
pub(crate) async fn view_profile_get(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
HxRequest(hx_request): HxRequest,
|
||||
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
||||
) -> axum::response::Result<Response> {
|
||||
) -> Result<ProfileView, WebError> {
|
||||
let uat: UserAuthToken = state
|
||||
.qe_r_ref
|
||||
.handle_whoami_uat(client_auth_info, kopid.eventid)
|
||||
.map_err(|op_err| HtmxError::new(&kopid, op_err))
|
||||
.await?;
|
||||
|
||||
let time = time::OffsetDateTime::now_utc() + time::Duration::new(60, 0);
|
||||
|
||||
let can_rw = uat.purpose_readwrite_active(time);
|
||||
|
||||
let profile_partial_view = ProfilePartialView {
|
||||
Ok(ProfileView {
|
||||
profile_partial: ProfilePartialView {
|
||||
menu_active_item: ProfileMenuItems::UserProfile,
|
||||
can_rw,
|
||||
account_name: uat.name().to_string(),
|
||||
display_name: uat.displayname.clone(),
|
||||
legal_name: uat.name().to_string(),
|
||||
email: uat.mail_primary.clone(),
|
||||
posix_enabled: false,
|
||||
};
|
||||
let profile_view = ProfileView {
|
||||
profile_partial: profile_partial_view.clone(),
|
||||
};
|
||||
|
||||
Ok(if hx_request {
|
||||
(
|
||||
HxPushUrl(Uri::from_static("/ui/profile")),
|
||||
HtmlTemplate(profile_partial_view),
|
||||
)
|
||||
.into_response()
|
||||
} else {
|
||||
HtmlTemplate(profile_view).into_response()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -81,12 +63,12 @@ pub(crate) async fn view_profile_unlock_get(
|
|||
DomainInfo(domain_info): DomainInfo,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
jar: CookieJar,
|
||||
) -> axum::response::Result<Response> {
|
||||
) -> Result<Response, HtmxError> {
|
||||
let uat: UserAuthToken = state
|
||||
.qe_r_ref
|
||||
.handle_whoami_uat(client_auth_info.clone(), kopid.eventid)
|
||||
.map_err(|op_err| HtmxError::new(&kopid, op_err))
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|op_err| HtmxError::new(&kopid, op_err))?;
|
||||
|
||||
let display_ctx = LoginDisplayCtx {
|
||||
domain_info,
|
||||
|
@ -94,15 +76,16 @@ pub(crate) async fn view_profile_unlock_get(
|
|||
username: uat.spn,
|
||||
purpose: ReauthPurpose::ProfileSettings,
|
||||
}),
|
||||
error: None,
|
||||
};
|
||||
|
||||
super::login::view_reauth_get(
|
||||
Ok(super::login::view_reauth_get(
|
||||
state,
|
||||
client_auth_info,
|
||||
kopid,
|
||||
jar,
|
||||
"/ui/profile",
|
||||
Urls::Profile.as_ref(),
|
||||
display_ctx,
|
||||
)
|
||||
.await
|
||||
.await)
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ use kanidm_proto::internal::{
|
|||
COOKIE_CU_SESSION_TOKEN,
|
||||
};
|
||||
|
||||
use super::constants::Urls;
|
||||
use crate::https::extractors::{DomainInfo, DomainInfoRead, VerifiedClientInformation};
|
||||
use crate::https::middleware::KOpId;
|
||||
use crate::https::views::constants::ProfileMenuItems;
|
||||
|
@ -31,7 +32,7 @@ use crate::https::views::errors::HtmxError;
|
|||
use crate::https::views::login::{LoginDisplayCtx, Reauth, ReauthPurpose};
|
||||
use crate::https::ServerState;
|
||||
|
||||
use super::{HtmlTemplate, UnrecoverableErrorView};
|
||||
use super::UnrecoverableErrorView;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "user_settings.html")]
|
||||
|
@ -226,7 +227,7 @@ pub(crate) async fn commit(
|
|||
Ok((HxLocation::from(Uri::from_static("/ui")), "").into_response())
|
||||
}
|
||||
|
||||
pub(crate) async fn cancel(
|
||||
pub(crate) async fn cancel_cred_update(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
HxRequest(_hx_request): HxRequest,
|
||||
|
@ -241,7 +242,11 @@ pub(crate) async fn cancel(
|
|||
.map_err(|op_err| HtmxError::new(&kopid, op_err))
|
||||
.await?;
|
||||
|
||||
Ok((HxLocation::from(Uri::from_static("/ui")), "").into_response())
|
||||
Ok((
|
||||
HxLocation::from(Uri::from_static(Urls::Profile.as_ref())),
|
||||
"",
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub(crate) async fn cancel_mfareg(
|
||||
|
@ -387,23 +392,23 @@ pub(crate) async fn view_new_passkey(
|
|||
let response = match cu_status.mfaregstate {
|
||||
CURegState::Passkey(chal) | CURegState::AttestedPasskey(chal) => {
|
||||
if let Ok(challenge) = serde_json::to_string(&chal) {
|
||||
HtmlTemplate(AddPasskeyPartial {
|
||||
AddPasskeyPartial {
|
||||
challenge,
|
||||
class: init_form.class,
|
||||
})
|
||||
}
|
||||
.into_response()
|
||||
} else {
|
||||
HtmlTemplate(UnrecoverableErrorView {
|
||||
UnrecoverableErrorView {
|
||||
err_code: OperationError::UI0001ChallengeSerialisation,
|
||||
operation_id: kopid.eventid,
|
||||
})
|
||||
}
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
_ => HtmlTemplate(UnrecoverableErrorView {
|
||||
_ => UnrecoverableErrorView {
|
||||
err_code: OperationError::UI0002InvalidState,
|
||||
operation_id: kopid.eventid,
|
||||
})
|
||||
}
|
||||
.into_response(),
|
||||
};
|
||||
|
||||
|
@ -472,7 +477,7 @@ pub(crate) async fn view_new_totp(
|
|||
)));
|
||||
};
|
||||
|
||||
return Ok((swapped_handler_trigger, push_url, HtmlTemplate(partial)).into_response());
|
||||
return Ok((swapped_handler_trigger, push_url, partial).into_response());
|
||||
}
|
||||
|
||||
// User has submitted a totp code
|
||||
|
@ -521,8 +526,12 @@ pub(crate) async fn view_new_totp(
|
|||
}
|
||||
};
|
||||
|
||||
let template = HtmlTemplate(AddTotpPartial { check_res });
|
||||
Ok((swapped_handler_trigger, push_url, template).into_response())
|
||||
Ok((
|
||||
swapped_handler_trigger,
|
||||
push_url,
|
||||
AddTotpPartial { check_res },
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub(crate) async fn view_new_pwd(
|
||||
|
@ -539,10 +548,13 @@ pub(crate) async fn view_new_pwd(
|
|||
|
||||
let new_passwords = match opt_form {
|
||||
None => {
|
||||
let partial = AddPasswordPartial {
|
||||
return Ok((
|
||||
swapped_handler_trigger,
|
||||
AddPasswordPartial {
|
||||
check_res: PwdCheckResult::Init,
|
||||
};
|
||||
return Ok((swapped_handler_trigger, HtmlTemplate(partial)).into_response());
|
||||
},
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
Some(Form(new_passwords)) => new_passwords,
|
||||
};
|
||||
|
@ -572,13 +584,12 @@ pub(crate) async fn view_new_pwd(
|
|||
pwd_equal,
|
||||
warnings,
|
||||
};
|
||||
let template = HtmlTemplate(AddPasswordPartial { check_res });
|
||||
|
||||
Ok((
|
||||
status,
|
||||
swapped_handler_trigger,
|
||||
HxPushUrl(Uri::from_static("/ui/reset/change_password")),
|
||||
template,
|
||||
AddPasswordPartial { check_res },
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
|
@ -619,17 +630,18 @@ pub(crate) async fn view_self_reset_get(
|
|||
username: uat.spn,
|
||||
purpose: ReauthPurpose::ProfileSettings,
|
||||
}),
|
||||
error: None,
|
||||
};
|
||||
|
||||
super::login::view_reauth_get(
|
||||
Ok(super::login::view_reauth_get(
|
||||
state,
|
||||
client_auth_info,
|
||||
kopid,
|
||||
jar,
|
||||
"/ui/update_credentials",
|
||||
Urls::UpdateCredentials.as_ref(),
|
||||
display_ctx,
|
||||
)
|
||||
.await
|
||||
.await)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -655,7 +667,7 @@ pub(crate) async fn view_reset_get(
|
|||
Query(params): Query<ResetTokenParam>,
|
||||
mut jar: CookieJar,
|
||||
) -> axum::response::Result<Response> {
|
||||
let push_url = HxPushUrl(Uri::from_static("/ui/reset"));
|
||||
let push_url = HxPushUrl(Uri::from_static(Urls::CredReset.as_ref()));
|
||||
let cookie = jar.get(COOKIE_CU_SESSION_TOKEN);
|
||||
let is_logged_in = state
|
||||
.qe_r_ref
|
||||
|
@ -684,10 +696,10 @@ pub(crate) async fn view_reset_get(
|
|||
jar = jar.remove(Cookie::from(COOKIE_CU_SESSION_TOKEN));
|
||||
|
||||
if let Some(token) = params.token {
|
||||
let token_uri_string = format!("/ui/reset?token={token}");
|
||||
return Ok((jar, Redirect::to(token_uri_string.as_str())).into_response());
|
||||
let token_uri_string = format!("{}?token={}", Urls::CredReset, token);
|
||||
return Ok((jar, Redirect::to(&token_uri_string)).into_response());
|
||||
}
|
||||
return Ok((jar, Redirect::to("/ui/reset")).into_response());
|
||||
return Ok((jar, Redirect::to(Urls::CredReset.as_ref())).into_response());
|
||||
}
|
||||
Err(op_err) => return Ok(HtmxError::new(&kopid, op_err).into_response()),
|
||||
};
|
||||
|
@ -709,25 +721,30 @@ pub(crate) async fn view_reset_get(
|
|||
Ok((jar, cu_resp).into_response())
|
||||
}
|
||||
Err(OperationError::SessionExpired) | Err(OperationError::Wait(_)) => {
|
||||
let cred_form_view = ResetCredFormView {
|
||||
// Reset code expired
|
||||
Ok((
|
||||
push_url,
|
||||
ResetCredFormView {
|
||||
domain_info,
|
||||
wrong_code: true,
|
||||
};
|
||||
|
||||
// Reset code expired
|
||||
Ok((push_url, HtmlTemplate(cred_form_view)).into_response())
|
||||
},
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
Err(op_err) => Err(ErrorResponse::from(
|
||||
HtmxError::new(&kopid, op_err).into_response(),
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
let cred_form_view = ResetCredFormView {
|
||||
// We don't have any credential, show reset token input form
|
||||
Ok((
|
||||
push_url,
|
||||
ResetCredFormView {
|
||||
domain_info,
|
||||
wrong_code: false,
|
||||
};
|
||||
// We don't have any credential, show reset token input form
|
||||
Ok((push_url, HtmlTemplate(cred_form_view)).into_response())
|
||||
},
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -759,11 +776,11 @@ fn get_cu_partial(cu_status: CUStatus) -> CredResetPartialView {
|
|||
fn get_cu_partial_response(cu_status: CUStatus) -> Response {
|
||||
let credentials_update_partial = get_cu_partial(cu_status);
|
||||
(
|
||||
HxPushUrl(Uri::from_static("/ui/reset")),
|
||||
HxPushUrl(Uri::from_static(Urls::CredReset.as_ref())),
|
||||
HxRetarget("#credentialUpdateDynamicSection".to_string()),
|
||||
HxReselect("#credentialUpdateDynamicSection".to_string()),
|
||||
HxReswap(SwapOption::OuterHtml),
|
||||
HtmlTemplate(credentials_update_partial),
|
||||
credentials_update_partial,
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
@ -788,23 +805,22 @@ fn get_cu_response(
|
|||
// TODO: fill in posix enabled
|
||||
posix_enabled: false,
|
||||
};
|
||||
let profile_view = ProfileView {
|
||||
profile_partial: cred_status_view,
|
||||
};
|
||||
|
||||
(
|
||||
HxPushUrl(Uri::from_static("/ui/update_credentials")),
|
||||
HtmlTemplate(profile_view),
|
||||
HxPushUrl(Uri::from_static(Urls::UpdateCredentials.as_ref())),
|
||||
ProfileView {
|
||||
profile_partial: cred_status_view,
|
||||
},
|
||||
)
|
||||
.into_response()
|
||||
} else {
|
||||
(
|
||||
HxPushUrl(Uri::from_static("/ui/reset")),
|
||||
HtmlTemplate(CredResetView {
|
||||
HxPushUrl(Uri::from_static(Urls::CredReset.as_ref())),
|
||||
CredResetView {
|
||||
domain_info,
|
||||
names,
|
||||
credentials_update_partial,
|
||||
}),
|
||||
},
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
@ -819,6 +835,10 @@ async fn get_cu_session(jar: CookieJar) -> Result<CUSessionToken, Response> {
|
|||
};
|
||||
Ok(cu_session_token)
|
||||
} else {
|
||||
Err((StatusCode::FORBIDDEN, Redirect::to("/ui/reset")).into_response())
|
||||
Err((
|
||||
StatusCode::FORBIDDEN,
|
||||
Redirect::to(Urls::CredReset.as_ref()),
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
</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="/ui/reset" 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"
|
||||
hx-post="/ui/reset/add_password"
|
||||
hx-include="#newPasswordForm"
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
Return to the home page
|
||||
</button>
|
||||
<button class="btn btn-primary"
|
||||
hx-get="/ui/reset"
|
||||
hx-get=(Urls::CredReset)
|
||||
hx-include="#token"
|
||||
hx-target="#cred-reset-form" hx-select="#cred-reset-form"
|
||||
hx-swap="outerHTML"
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
<div class="d-flex align-items-start form-cred-reset-body">
|
||||
<div class="w-100">
|
||||
<div class="py-3">
|
||||
<p>(( names ))</p>
|
||||
<p>(( domain_info.display_name() ))</p>
|
||||
<p><strong>User:</strong> (( names ))</p>
|
||||
<p><strong>Kanidm domain:</strong> (( domain_info.display_name() ))</p>
|
||||
</div>
|
||||
(( credentials_update_partial|safe ))
|
||||
</div>
|
||||
|
|
|
@ -130,7 +130,7 @@
|
|||
<div class="mt-2 pt-2 border-top">
|
||||
<button class="btn btn-danger"
|
||||
hx-post="/ui/api/cu_cancel"
|
||||
hx-target="body">Discard Changes</button>
|
||||
hx-target="#main">Discard Changes</button>
|
||||
<span class="d-inline-block" tabindex="0"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-title="Unresolved warnings">
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
(% extends "login_base.html" %)
|
||||
|
||||
(% block logincontainer %)
|
||||
(% if let Some(error) = display_ctx.error %)
|
||||
<div class="alert alert-danger" role="alert">
|
||||
(( error ))
|
||||
</div>
|
||||
(% endif %)
|
||||
|
||||
|
||||
<label for="username" class="form-label">Username</label>
|
||||
<form id="login" action="/ui/login/begin" method="post">
|
||||
<div class="input-group mb-3">
|
||||
|
|
|
@ -18,9 +18,11 @@
|
|||
(% endif %)
|
||||
<h3>Kanidm</h3>
|
||||
(% if let Some(reauth) = display_ctx.reauth %)
|
||||
<h5>Reauthenticating as (( reauth.username )) to access (( reauth.purpose ))</h5>
|
||||
<h5>Reauthenticating as (( reauth.username )) to access (( reauth.purpose
|
||||
))</h5>
|
||||
(% endif %)
|
||||
</center>
|
||||
|
||||
<div id="login-form-container" class="container">
|
||||
(% block logincontainer %)
|
||||
(% endblock %)
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<main id="main">
|
||||
<p>Reason: (( reason ))</p>
|
||||
<p>Operation ID: (( operation_id ))</p>
|
||||
<a href="/ui/login">
|
||||
<a href=((Urls::Login.as_ref()))>
|
||||
<button type="button" class="btn btn-success">Return to Login</button>
|
||||
</a>
|
||||
</main>
|
||||
|
|
|
@ -16,14 +16,12 @@
|
|||
<div class="collapse navbar-collapse" id="navbarCollapse">
|
||||
<ul class="(( crate::https::ui::CSS_NAVBAR_LINKS_UL ))">
|
||||
<li class="mb-1">
|
||||
<a class="nav-link" href="/ui/apps" hx-target="main"
|
||||
hx-select="main" hx-swap="outerHTML"><span
|
||||
data-feather="file"></span>Apps</a>
|
||||
<a class="nav-link" href=((Urls::Apps))>
|
||||
<span data-feather="file"></span>Apps</a>
|
||||
</li>
|
||||
<li class="mb-1">
|
||||
<a class="nav-link" href="/ui/profile" hx-target="main"
|
||||
hx-select="main" hx-swap="outerHTML"><span
|
||||
data-feather="file"></span>Profile</a>
|
||||
<a class="nav-link" href=((Urls::Profile))>
|
||||
<span data-feather="file"></span>Profile</a>
|
||||
</li>
|
||||
<li class="mb-1">
|
||||
<a class="nav-link" href="#" data-bs-toggle="modal"
|
||||
|
|
|
@ -8,18 +8,16 @@
|
|||
</a>
|
||||
(% endmacro %)
|
||||
|
||||
<main class="container-xxl pb-5">
|
||||
<main id="main" class="container-xxl pb-5">
|
||||
<div class="d-flex flex-sm-row flex-column">
|
||||
<div class="list-group side-menu flex-shrink-0">
|
||||
(% call side_menu_item("Profile", "/ui/profile",
|
||||
(% call side_menu_item("Profile", (Urls::Profile),
|
||||
ProfileMenuItems::UserProfile, "person") %)
|
||||
(% call side_menu_item("SSH Keys", "/ui/ssh_keys",
|
||||
ProfileMenuItems::SshKeys, "key") %)
|
||||
(% if posix_enabled %)
|
||||
(% call side_menu_item("UNIX Password", "/ui/update_credentials",
|
||||
(% call side_menu_item("UNIX Password", (Urls::UpdateCredentials),
|
||||
ProfileMenuItems::UnixPassword, "building-lock") %)
|
||||
(% endif %)
|
||||
(% call side_menu_item("Credentials", "/ui/update_credentials",
|
||||
(% call side_menu_item("Credentials", (Urls::UpdateCredentials),
|
||||
ProfileMenuItems::Credentials, "shield-lock") %)
|
||||
</div>
|
||||
<div id="settings-window" class="flex-grow-1 ps-sm-4 pt-sm-0 pt-4">
|
||||
|
|
|
@ -13,8 +13,8 @@ Unix/Posix Settings
|
|||
(% if can_rw %)
|
||||
<button class="btn btn-primary" type="button" hx-post="/ui/api/user_settings/edit_posix">Edit</button>
|
||||
(% else %)
|
||||
<a href="/ui/profile/unlock" hx-boost="false">
|
||||
<button class="btn btn-primary" type="button">Unlock Edit 🔒</button>
|
||||
<a href=(Urls::ProfileUnlock) hx-boost="false">
|
||||
<button class="btn btn-primary" type="button">((UiMessage::UnlockEdit))</button>
|
||||
</a>
|
||||
(% endif %)
|
||||
(% endblock %)
|
||||
|
|
|
@ -17,34 +17,27 @@ Profile
|
|||
<div class="mb-2 row">
|
||||
<label for="profileDisplayName" class="col-12 col-md-3 col-lg-2 col-form-label">Display name</label>
|
||||
<div class="col-12 col-md-6 col-lg-4">
|
||||
<input type="text" class="form-control" id="profileDisplayName" value="(( display_name ))">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-2 row">
|
||||
<label for="profileLegalName" class="col-12 col-md-3 col-lg-2 col-form-label">Legal name</label>
|
||||
<div class="col-12 col-md-6 col-lg-4">
|
||||
<input type="text" class="form-control" id="profileLegalName" value="(( legal_name ))">
|
||||
<input type="text" class="form-control-plaintext" id="profileDisplayName" value="(( display_name ))" disabled>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-2 row">
|
||||
<label for="profileEmail" class="col-12 col-md-3 col-lg-2 col-form-label">Email</label>
|
||||
<div class="col-12 col-md-6 col-lg-4">
|
||||
<input type="email" class="form-control" id="profileEmail" (% if let Some(email) = email %)value="(( email ))"(% endif %)>
|
||||
<input type="email" disabled class="form-control-plaintext" id="profileEmail" value="(( email.clone().unwrap_or("None configured".to_string())))" >
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit button -->
|
||||
<div class="pt-4">
|
||||
<!-- <div class="pt-4">
|
||||
(% if can_rw %)
|
||||
<button class="btn btn-primary" type="button" hx-post="/ui/api/user_settings/edit_profile">Edit</button>
|
||||
<button class="btn btn-primary" type="button" hx-post="/ui/api/user_settings/edit_profile" disabled>Edit (Currently Not Working!)</button>
|
||||
(% else %)
|
||||
<a href="/ui/profile/unlock" hx-boost="false">
|
||||
<button class="btn btn-primary" type="button">Unlock Edit 🔒</button>
|
||||
<a href=(Urls::ProfileUnlock) hx-boost="false">
|
||||
<button class="btn btn-primary" type="button">((UiMessage::UnlockEdit))</button>
|
||||
</a>
|
||||
(% endif %)
|
||||
</div>
|
||||
</div> -->
|
||||
</form>
|
||||
|
||||
(% endblock %)
|
||||
|
|
Loading…
Reference in a new issue