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_api_router, 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 radius;
31mod reset;
32
33#[derive(Template)]
34#[template(path = "unrecoverable_error.html")]
35struct UnrecoverableErrorView {
36 err_code: OperationError,
37 operation_id: Uuid,
38 domain_info: DomainInfoRead,
40}
41
42#[derive(Template)]
43#[template(path = "admin/error_toast.html")]
44struct ErrorToastPartial {
45 err_code: OperationError,
46 operation_id: Uuid,
47}
48
49pub fn view_router() -> Router<ServerState> {
50 let mut unguarded_router = Router::new()
51 .route(
52 "/",
53 get(|| async { Redirect::permanent(Urls::Login.as_ref()) }),
54 )
55 .route("/apps", get(apps::view_apps_get))
56 .route("/enrol", get(enrol::view_enrol_get))
57 .route("/reset", get(reset::view_reset_get))
58 .route("/update_credentials", get(reset::view_self_reset_get))
59 .route("/profile", get(profile::view_profile_get))
60 .route("/profile/diff", get(profile::view_profile_get))
61 .route("/radius", get(radius::view_radius_get))
62 .route("/unlock", get(login::view_reauth_to_referer_get))
63 .route("/logout", get(login::view_logout_get))
64 .route("/oauth2", get(oauth2::view_index_get));
65
66 #[cfg(feature = "dev-oauth2-device-flow")]
67 {
68 unguarded_router = unguarded_router.route(
69 kanidmd_lib::prelude::uri::OAUTH2_DEVICE_LOGIN,
70 get(oauth2::view_device_get).post(oauth2::view_device_post),
71 );
72 }
73 unguarded_router = unguarded_router
74 .route("/oauth2/resume", get(oauth2::view_resume_get))
75 .route("/oauth2/consent", post(oauth2::view_consent_post))
76 .route("/login", get(login::view_index_get))
80 .route(
81 "/login/passkey",
82 post(login::view_login_passkey_post).get(|| async { Redirect::to("/ui") }),
83 )
84 .route(
85 "/login/seckey",
86 post(login::view_login_seckey_post).get(|| async { Redirect::to("/ui") }),
87 )
88 .route(
89 "/login/begin",
90 post(login::view_login_begin_post).get(|| async { Redirect::to("/ui") }),
91 )
92 .route(
93 "/login/mech_choose",
94 post(login::view_login_mech_choose_post).get(|| async { Redirect::to("/ui") }),
95 )
96 .route(
97 "/login/backup_code",
98 post(login::view_login_backupcode_post).get(|| async { Redirect::to("/ui") }),
99 )
100 .route(
101 "/login/totp",
102 post(login::view_login_totp_post).get(|| async { Redirect::to("/ui") }),
103 )
104 .route(
105 "/login/pw",
106 post(login::view_login_pw_post).get(|| async { Redirect::to("/ui") }),
107 );
108
109 let guarded_router = Router::new()
113 .route("/reset/add_totp", post(reset::view_new_totp))
114 .route("/reset/add_password", post(reset::view_new_pwd))
115 .route("/reset/change_password", post(reset::view_new_pwd))
116 .route("/reset/add_passkey", post(reset::view_new_passkey))
117 .route("/reset/set_unixcred", post(reset::view_set_unixcred))
118 .route(
119 "/reset/add_ssh_publickey",
120 post(reset::view_add_ssh_publickey),
121 )
122 .route("/radius/generate", post(radius::view_radius_post))
123 .route("/api/delete_alt_creds", post(reset::remove_alt_creds))
124 .route("/api/delete_unixcred", post(reset::remove_unixcred))
125 .route("/api/add_totp", post(reset::add_totp))
126 .route("/api/remove_totp", post(reset::remove_totp))
127 .route("/api/remove_passkey", post(reset::remove_passkey))
128 .route("/api/finish_passkey", post(reset::finish_passkey))
129 .route("/api/cancel_mfareg", post(reset::cancel_mfareg))
130 .route(
131 "/api/remove_ssh_publickey",
132 post(reset::remove_ssh_publickey),
133 )
134 .route("/api/cu_cancel", post(reset::cancel_cred_update))
135 .route("/api/cu_commit", post(reset::commit))
136 .route(
137 "/api/user_settings/add_email",
138 get(profile::view_new_email_entry_partial),
139 )
140 .route(
141 "/api/user_settings/edit_profile",
142 post(profile::view_profile_diff_start_save_post),
143 )
144 .route(
145 "/api/user_settings/confirm_profile",
146 post(profile::view_profile_diff_confirm_save_post),
147 )
148 .layer(HxRequestGuardLayer::new("/ui"));
149
150 let admin_router = admin_router();
151 let admin_api_router = admin_api_router();
152 Router::new()
153 .merge(unguarded_router)
154 .merge(guarded_router)
155 .nest("/admin", admin_router)
156 .nest("/api/admin", admin_api_router)
157}
158
159fn empty_string_as_none<'de, D, T>(de: D) -> Result<Option<T>, D::Error>
161where
162 D: serde::Deserializer<'de>,
163 T: std::str::FromStr,
164 T::Err: std::fmt::Display,
165{
166 use serde::Deserialize;
167 use std::str::FromStr;
168
169 let opt = Option::<String>::deserialize(de)?;
170 match opt.as_deref() {
171 None | Some("") => Ok(None),
172 Some(s) => FromStr::from_str(s)
173 .map_err(serde::de::Error::custom)
174 .map(Some),
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use askama_axum::IntoResponse;
181
182 use super::*;
183 #[tokio::test]
184 async fn test_unrecoverableerrorview() {
185 let domain_info = kanidmd_lib::server::DomainInfo::new_test();
186
187 let view = UnrecoverableErrorView {
188 err_code: OperationError::InvalidState,
189 operation_id: Uuid::new_v4(),
190 domain_info: domain_info.read(),
191 };
192
193 let error_html = view.render().expect("Failed to render");
194
195 assert!(error_html.contains(domain_info.read().display_name()));
196
197 let response = view.into_response();
198
199 assert_eq!(response.status(), 200);
201 }
202}