1use axum::response::{IntoResponse, Redirect, Result};
4use axum::{extract::State, response::Response};
5use axum_typed_multipart::TryFromMultipart;
6use serde::{Deserialize, Serialize};
7
8use crate::formats::ip::get_regex_ipv4_ipv6_exact;
9use crate::io::webui::controllers::base::redirect_temporary;
10use crate::io::webui::error::{WebErr, WebError, WebResult};
11use crate::io::webui::flexible_form::FlexibleForm;
12use crate::io::webui::session_auth::AuthenticatedUser;
13use crate::io::webui::{AppState, RequestState, error_403, respond_page};
14use crate::storage::pc_settings::{DEFAULT_BIND_TO_IP, DEFAULT_SERVER_URL};
15use crate::storage::user::UserPublicInfo;
16use crate::utilities::build_info;
17use crate::utilities::password::{Password, hash, verify};
18use crate::{debug_fmt, json_value};
19
20pub async fn get_home(
22 State(state): State<AppState>,
23 req: RequestState,
24 user: AuthenticatedUser,
25) -> Response {
26 respond_page(&state, req, "home", &json_value!({}))
27}
28
29pub async fn get_public_pc_settings(
34 State(state): State<AppState>,
35 req: RequestState,
36) -> WebResult<Response> {
37 #[derive(Serialize, Debug)]
38 struct UserWithAdmin {
39 admin: bool,
40 local_id: u64,
41 username: String,
42 display_name: Option<Vec<u8>>,
43 picture: Option<Vec<u8>>,
44 }
45
46 let build_info = build_info();
47 let current_settings =
48 crate::storage::pc_settings::PcSettings::load().unwrap_or_default();
49
50 let all_users = Vec::<UserPublicInfo>::new();
54
55 debug_fmt!("All users: {all_users:#?}");
56
57 let users: Vec<UserWithAdmin> = all_users
58 .into_iter()
59 .map(|u| UserWithAdmin {
60 admin: current_settings.admin_users.contains(&u.local_id()),
61 local_id: u.local_id(),
62 username: u.name().to_string(),
63 display_name: u.display_name().map(<[u8]>::to_vec),
64 picture: u.user_picture().map(<[u8]>::to_vec),
65 })
66 .collect();
67
68 debug_fmt!("Rendering PC settings page with users: {users:#?}");
69
70 Ok(respond_page(
71 &state,
72 req,
73 "settings.pc-settings",
74 &json_value! ({
75 "show_users" => current_settings.show_users,
76 "bind_to_ip" => current_settings.bind_to_ip,
77 "bind_to_ip_regex" => get_regex_ipv4_ipv6_exact(),
78 "server_url" => current_settings.server_url,
79 "domain_name" => current_settings.domain_name.unwrap_or_default(),
80 "fixed_http_port" => current_settings.fixed_http_port.unwrap_or(0),
81 "fixed_https_port" => current_settings.fixed_https_port.unwrap_or(0),
82 "tls_certificate" => current_settings.tls_certificate.unwrap_or_default(),
83 "kiosk_mode" => current_settings.kiosk_mode,
84 "log_stack_file" => current_settings.log_stack_file,
85
86 "feature_login" => current_settings.feature_login,
88 "feature_registration" => current_settings.feature_registration,
89
90 "tls_private_key_set" => current_settings.tls_private_key.is_some(),
92 "admin_password_set" => current_settings.admin_password_hash.is_some(),
93
94 "users" => users,
96
97 "crate_name" => build_info.name,
99 "crate_version" => build_info.version,
100 "build_date" => build_info.build_date,
101 "commit" => build_info.commit
102 }),
103 ))
104}
105
106#[derive(TryFromMultipart, Serialize, Deserialize)]
107#[try_from_multipart(strict)]
108pub struct PcSettingsForm {
109 show_users: Option<bool>,
110 bind_to_ip: Option<String>,
111 server_url: Option<String>,
112 domain_name: String,
113 fixed_http_port: u16,
114 fixed_https_port: u16,
115 http_redirect: Option<bool>,
116 tls_certificate: Option<String>,
117 tls_private_key: Option<String>,
118 admin_users: Vec<u64>,
119 admin_password: Option<String>,
120 kiosk_mode: Option<bool>,
121 log_stack_file: Option<bool>,
122 feature_login: Option<bool>,
123 feature_registration: Option<bool>,
124}
125
126#[allow(clippy::too_many_lines)]
127pub async fn post_public_pc_settings(
128 State(state): State<AppState>,
129 req: RequestState,
130 form: FlexibleForm<PcSettingsForm>,
131) -> WebResult<Response> {
132 let PcSettingsForm {
133 show_users,
134 bind_to_ip,
135 server_url,
136 domain_name,
137 fixed_http_port,
138 fixed_https_port,
139 http_redirect,
140 tls_certificate,
141 tls_private_key,
142 admin_users,
143 admin_password,
144 kiosk_mode,
145 log_stack_file,
146 feature_login,
147 feature_registration,
148 } = form.0;
149
150 let admin_password: Option<Password> = admin_password
151 .map(|admin_password| Password::from_string(&admin_password));
152
153 let current_settings =
154 crate::storage::pc_settings::PcSettings::load().unwrap_or_default();
155 if let Some(ref current_pass_hash) = current_settings.admin_password_hash {
157 match admin_password.clone() {
158 Some(provided)
159 if verify(&provided, current_pass_hash.as_str()).map_err(
160 |e| WebError::new(e, state.clone(), req.clone()),
161 )? => {}
162 _ => {
163 return Ok(error_403(
164 &state,
165 &req,
166 "Invalid admin password".to_string(),
167 ));
168 }
169 }
170 }
171 let admin_password_hash =
173 if current_settings.admin_password_hash.is_none() {
174 if let Some(provided) = admin_password {
175 Some(hash(&provided).map_err(|e| {
176 WebError::new(e, state.clone(), req.clone())
177 })?)
178 } else {
179 None
180 }
181 } else {
182 current_settings.admin_password_hash.clone()
183 };
184
185 let tls_private_key = if tls_private_key.is_some() {
186 tls_private_key
187 } else {
188 current_settings.tls_private_key.clone()
189 };
190
191 let settings = crate::storage::pc_settings::PcSettings {
192 show_users: if let Some(show_users) = show_users {
193 show_users
194 } else {
195 current_settings.show_users
196 },
197 bind_to_ip: if let Some(bind_to_ip) = bind_to_ip {
198 if bind_to_ip.is_empty() {
199 DEFAULT_BIND_TO_IP.to_string()
200 } else {
201 bind_to_ip
202 }
203 } else {
204 current_settings.bind_to_ip
205 },
206 server_url: if let Some(server_url) = server_url {
207 if server_url.is_empty() {
208 DEFAULT_SERVER_URL.to_string()
209 } else {
210 server_url
211 }
212 } else {
213 current_settings.server_url
214 },
215 domain_name: if domain_name.is_empty() {
216 None
217 } else {
218 Some(domain_name)
219 },
220 fixed_http_port: if fixed_http_port == 0 {
221 None
222 } else {
223 Some(fixed_http_port)
224 },
225 fixed_https_port: if fixed_https_port == 0 {
226 None
227 } else {
228 Some(fixed_https_port)
229 },
230 http_redirect: if let Some(http_redirect) = http_redirect {
231 http_redirect
232 } else {
233 current_settings.http_redirect
234 },
235 tls_certificate,
236 tls_private_key,
237 admin_users,
238 admin_password_hash,
239 kiosk_mode: if let Some(kiosk_mode) = kiosk_mode {
240 kiosk_mode
241 } else {
242 current_settings.kiosk_mode
243 },
244 log_stack_file: if let Some(log_stack_file) = log_stack_file {
245 log_stack_file
246 } else {
247 current_settings.log_stack_file
248 },
249 feature_login: if let Some(feature_login) = feature_login {
250 feature_login
251 } else {
252 current_settings.feature_login
253 },
254 feature_registration: if let Some(feature_registration) =
255 feature_registration
256 {
257 feature_registration
258 } else {
259 current_settings.feature_registration
260 },
261 ..Default::default()
262 };
263
264 settings.save().web_err(&state, &req)?;
265 Ok(redirect_temporary(req.is_js_request, "/pc-settings"))
266}
267
268#[cfg(test)]
269#[allow(clippy::unwrap_in_result, clippy::panic_in_result_fn)]
270mod tests {
271 use crate::io::webui::test_helpers::{
272 assert_body_contains, assert_eq_or_print_body, test_get_no_login,
273 test_post_no_login,
274 };
275 use crate::storage::pc_settings::PcSettings;
276 use crate::storage::user::get_test_user;
277
278 #[crate::ctb_test(tokio::test)]
279 async fn can_get_pc_settings() {
280 let test_user_1 =
281 get_test_user(format!("{}_1", function_name!()).as_str());
282 let test_user_2 =
283 get_test_user(format!("{}_2", function_name!()).as_str());
284 let test_user_3 =
285 get_test_user(format!("{}_3", function_name!()).as_str());
286
287 let settings = PcSettings {
289 show_users: true,
290 bind_to_ip: "123.456.789".to_string(),
291 domain_name: Some("foo.com".to_string()),
292 fixed_http_port: Some(1234),
293 fixed_https_port: Some(4567),
294 admin_users: vec![test_user_1.local_id(), test_user_3.local_id()],
295 ..Default::default()
296 };
297 settings.save().unwrap();
298
299 let (status, body) = test_get_no_login("/pc-settings").await;
300 assert_eq_or_print_body(status, 200, &body);
301 assert_body_contains(
302 "These options apply to all users on this computer",
303 &body,
304 );
305 assert_body_contains("value=\"foo.com\"", &body);
307 assert_body_contains("value=\"1234\"", &body);
308 }
323
324 #[crate::ctb_test(tokio::test)]
325 async fn can_save_settings_and_read_back() {
326 #[derive(serde::Serialize)]
328 struct Form {
329 show_users: bool,
330 bind_to_ip: Option<String>,
331 domain_name: Option<String>,
332 fixed_http_port: Option<u16>,
333 fixed_https_port: Option<u16>,
334 admin_users: Vec<u64>,
335 feature_login: bool,
336 }
337 let form = Form {
338 show_users: false,
339 bind_to_ip: Some("127.0.0.1".to_string()),
340 domain_name: Some("example.com".to_string()),
341 fixed_http_port: Some(5678),
342 fixed_https_port: Some(5679),
343 admin_users: vec![18, 19],
344 feature_login: true,
345 };
346
347 let (status, body) =
348 test_post_no_login("/pc-settings", None, Some(&form), None).await;
349 assert_eq_or_print_body(status, 303, &body); let loaded = PcSettings::load().unwrap();
353 assert!(!loaded.show_users);
354 assert_eq!(loaded.bind_to_ip, "127.0.0.1".to_string());
355 assert_eq!(loaded.domain_name, Some("example.com".to_string()));
356 assert_eq!(loaded.fixed_http_port, Some(5678));
357 assert_eq!(loaded.fixed_https_port, Some(5679));
358 assert_eq!(loaded.admin_users, vec![18, 19]);
359 assert!(loaded.feature_login);
360 }
361}