kanidmd_core/https/views/
mod.rs
1use askama::Template;
2
3use axum::{
4 response::Redirect,
5 routing::{get, post},
6 Router,
7};
8
9use axum_htmx::HxRequestGuardLayer;
10
11use crate::https::views::admin::admin_router;
12use constants::Urls;
13use kanidmd_lib::{
14 idm::server::DomainInfoRead,
15 prelude::{OperationError, Uuid},
16};
17
18use crate::https::ServerState;
19
20mod admin;
21mod apps;
22pub(crate) mod constants;
23mod cookies;
24mod enrol;
25mod errors;
26mod login;
27mod navbar;
28mod oauth2;
29mod profile;
30mod reset;
31
32#[derive(Template)]
33#[template(path = "unrecoverable_error.html")]
34struct UnrecoverableErrorView {
35 err_code: OperationError,
36 operation_id: Uuid,
37 domain_info: DomainInfoRead,
39}
40
41#[derive(Template)]
42#[template(path = "admin/error_toast.html")]
43struct ErrorToastPartial {
44 err_code: OperationError,
45 operation_id: Uuid,
46}
47
48pub fn view_router() -> Router<ServerState> {
49 let mut unguarded_router = Router::new()
50 .route(
51 "/",
52 get(|| async { Redirect::permanent(Urls::Login.as_ref()) }),
53 )
54 .route("/apps", get(apps::view_apps_get))
55 .route("/enrol", get(enrol::view_enrol_get))
56 .route("/reset", get(reset::view_reset_get))
57 .route("/update_credentials", get(reset::view_self_reset_get))
58 .route("/profile", get(profile::view_profile_get))
59 .route("/profile/unlock", get(profile::view_profile_unlock_get))
60 .route("/logout", get(login::view_logout_get))
61 .route("/oauth2", get(oauth2::view_index_get));
62
63 #[cfg(feature = "dev-oauth2-device-flow")]
64 {
65 unguarded_router = unguarded_router.route(
66 kanidmd_lib::prelude::uri::OAUTH2_DEVICE_LOGIN,
67 get(oauth2::view_device_get).post(oauth2::view_device_post),
68 );
69 }
70 unguarded_router = unguarded_router
71 .route("/oauth2/resume", get(oauth2::view_resume_get))
72 .route("/oauth2/consent", post(oauth2::view_consent_post))
73 .route("/login", get(login::view_index_get))
77 .route(
78 "/login/passkey",
79 post(login::view_login_passkey_post).get(|| async { Redirect::to("/ui") }),
80 )
81 .route(
82 "/login/seckey",
83 post(login::view_login_seckey_post).get(|| async { Redirect::to("/ui") }),
84 )
85 .route(
86 "/login/begin",
87 post(login::view_login_begin_post).get(|| async { Redirect::to("/ui") }),
88 )
89 .route(
90 "/login/mech_choose",
91 post(login::view_login_mech_choose_post).get(|| async { Redirect::to("/ui") }),
92 )
93 .route(
94 "/login/backup_code",
95 post(login::view_login_backupcode_post).get(|| async { Redirect::to("/ui") }),
96 )
97 .route(
98 "/login/totp",
99 post(login::view_login_totp_post).get(|| async { Redirect::to("/ui") }),
100 )
101 .route(
102 "/login/pw",
103 post(login::view_login_pw_post).get(|| async { Redirect::to("/ui") }),
104 );
105
106 let guarded_router = Router::new()
110 .route("/reset/add_totp", post(reset::view_new_totp))
111 .route("/reset/add_password", post(reset::view_new_pwd))
112 .route("/reset/change_password", post(reset::view_new_pwd))
113 .route("/reset/add_passkey", post(reset::view_new_passkey))
114 .route("/reset/set_unixcred", post(reset::view_set_unixcred))
115 .route(
116 "/reset/add_ssh_publickey",
117 post(reset::view_add_ssh_publickey),
118 )
119 .route("/api/delete_alt_creds", post(reset::remove_alt_creds))
120 .route("/api/delete_unixcred", post(reset::remove_unixcred))
121 .route("/api/add_totp", post(reset::add_totp))
122 .route("/api/remove_totp", post(reset::remove_totp))
123 .route("/api/remove_passkey", post(reset::remove_passkey))
124 .route("/api/finish_passkey", post(reset::finish_passkey))
125 .route("/api/cancel_mfareg", post(reset::cancel_mfareg))
126 .route(
127 "/api/remove_ssh_publickey",
128 post(reset::remove_ssh_publickey),
129 )
130 .route("/api/cu_cancel", post(reset::cancel_cred_update))
131 .route("/api/cu_commit", post(reset::commit))
132 .layer(HxRequestGuardLayer::new("/ui"));
133
134 let admin_router = admin_router();
135 Router::new()
136 .merge(unguarded_router)
137 .merge(guarded_router)
138 .nest("/admin", admin_router)
139}
140
141fn empty_string_as_none<'de, D, T>(de: D) -> Result<Option<T>, D::Error>
143where
144 D: serde::Deserializer<'de>,
145 T: std::str::FromStr,
146 T::Err: std::fmt::Display,
147{
148 use serde::Deserialize;
149 use std::str::FromStr;
150
151 let opt = Option::<String>::deserialize(de)?;
152 match opt.as_deref() {
153 None | Some("") => Ok(None),
154 Some(s) => FromStr::from_str(s)
155 .map_err(serde::de::Error::custom)
156 .map(Some),
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use askama_axum::IntoResponse;
163
164 use super::*;
165 #[tokio::test]
166 async fn test_unrecoverableerrorview() {
167 let domain_info = kanidmd_lib::server::DomainInfo::new_test();
168
169 let view = UnrecoverableErrorView {
170 err_code: OperationError::InvalidState,
171 operation_id: Uuid::new_v4(),
172 domain_info: domain_info.read(),
173 };
174
175 let error_html = view.render().expect("Failed to render");
176
177 assert!(error_html.contains(domain_info.read().display_name()));
178
179 let response = view.into_response();
180
181 assert_eq!(response.status(), 200);
183 }
184}