1use std::sync::Arc;
5
6use toasty::Db;
7use topcoat::{
8 Result,
9 asset::AssetConfig,
10 context::Cx,
11 cookie::RouterBuilderCookieExt,
12 router::{
13 Body, LayoutFn, Methods, PageFn, Path, RouteFn, RouteFuture, RouterBuilder,
14 RouterBuilderDirectoryExt, error::redirect,
15 },
16 runtime::{PrefetchMode, RouterBuilderRuntimeExt, RuntimeSetup},
17 session::{RouterBuilderSessionExt, SessionConfig},
18};
19
20use super::{
21 Panel, Root, error_page,
22 forms::MAX_FORM_BYTES,
23 headers::{self, ServedDir},
24 host::PanelHost,
25 register::Registry,
26 state::{PanelState, Panels, current, under_prefix},
27};
28use crate::{
29 DeclarationError, DeclarationErrorKind, MountError, Site,
30 auth::{AuthHost, PanelGate, Realm, RuntimeGate, SESSION_LIFETIME},
31 bus::NotificationBus,
32 declaration::segment_fault,
33 naming::route_path,
34 resource::{MountScope, Mounts},
35 tenancy::TenantSource,
36 toasty_compat::model::AppSchema,
37 topcoat_compat::RUNTIME_PREFIX,
38};
39
40mod handle;
41
42pub use self::handle::PanelHandle;
43use self::handle::request_mounts;
44
45pub trait RouterBuilderPanelExt: Sized {
82 fn panel(self, panel: Panel) -> Result<Self>;
96
97 fn panel_handle(&self, prefix: &str) -> Option<PanelHandle>;
101}
102
103impl RouterBuilderPanelExt for RouterBuilder {
104 fn panel(self, panel: Panel) -> Result<Self> {
105 panel.mount(self)
106 }
107
108 fn panel_handle(&self, prefix: &str) -> Option<PanelHandle> {
109 let panel = self
110 .get_app_context::<Panels>()?
111 .by_prefix(&crate::naming::normalize_prefix(prefix))?;
112 Some(PanelHandle {
113 db: self.get_app_context::<Db>()?.clone(),
114 mounts: Arc::clone(&panel.mounts),
115 panel: Some(Arc::clone(panel)),
116 prefix: panel.realm.prefix.clone(),
117 bus: self
118 .get_app_context::<NotificationBus>()
119 .cloned()
120 .unwrap_or_else(NotificationBus::new),
121 })
122 }
123}
124
125impl Panel {
126 pub fn handle(self, db: &Db) -> Result<PanelHandle> {
135 let Panel {
136 prefix,
137 registrations,
138 configuration_errors,
139 uploads,
140 ..
141 } = self;
142 let mut registry = Registry::new(prefix.clone(), Some(AppSchema::of_db(db)));
143 let (mounts, mut errors) = registry.register_all(registrations, configuration_errors);
144 registry.check_all(db, &mounts, uploads.is_some(), &mut errors);
145 if !errors.is_empty() {
146 return Err(MountError::new(&prefix, errors).into());
147 }
148 Ok(PanelHandle {
149 db: db.clone(),
150 mounts,
151 panel: None,
152 prefix,
153 bus: NotificationBus::new(),
154 })
155 }
156
157 fn mount(self, mut builder: RouterBuilder) -> Result<RouterBuilder> {
158 let db = builder.get_app_context::<Db>().cloned();
159 let services = crate::auth::mount::Services {
160 mail: builder
161 .get_app_context::<topcoat::mail::MailConfig>()
162 .is_some(),
163 cookie_key: builder.get_app_context::<topcoat::cookie::Key>().is_some(),
164 };
165 let mut registry = Registry::new(self.prefix.clone(), db.as_ref().map(AppSchema::of_db));
166 let Panel {
167 prefix,
168 shell_assets,
169 brand,
170 dark_mode,
171 layout,
172 registrations,
173 content_security_policy,
174 strict_transport_security,
175 configuration_errors,
176 uploads,
177 served_dirs,
178 login_hint,
179 user_menu,
180 auth,
181 mut endpoints,
182 notifications,
183 time_zone,
184 locales,
185 translations,
186 global_search,
187 } = self;
188 let (mounts, mut errors) = registry.register_all(registrations, configuration_errors);
189 errors.extend(mount_errors(
190 &builder,
191 &prefix,
192 shell_assets.is_some(),
193 &served_dirs,
194 ));
195 match &db {
196 Some(db) => {
197 registry.check_all(db, &mounts, uploads.is_some(), &mut errors);
198 let toggles = registry.resources.iter().any(|resource| resource.toggles);
199 errors.extend(
200 crate::auth::mount::declaration_errors(
201 db,
202 &auth,
203 toggles,
204 notifications,
205 services,
206 )
207 .into_iter()
208 .map(DeclarationError::panel),
209 );
210 }
211 None => errors.push(DeclarationError::panel(DeclarationErrorKind::MissingDb)),
212 }
213 let catalog = match crate::i18n::Catalog::new(&locales, &translations) {
214 Ok(catalog) => Some(catalog),
215 Err(faults) => {
216 errors.extend(
217 faults
218 .into_iter()
219 .map(|fault| DeclarationError::panel(DeclarationErrorKind::Locale(fault))),
220 );
221 None
222 }
223 };
224 let panel_headers = match headers::PanelHeaders::new(
225 prefix.clone(),
226 content_security_policy.resolve().as_deref(),
227 strict_transport_security,
228 ) {
229 Ok(layer) => Some(layer),
230 Err(kind) => {
231 errors.push(DeclarationError::panel(kind));
232 None
233 }
234 };
235 if !errors.is_empty() {
236 return Err(MountError::new(&prefix, errors).into());
237 }
238 let catalog = catalog.expect("a catalog that failed to build fails the mount");
239 if builder.get_app_context::<Panels>().is_none() {
240 builder = install_shared(builder);
241 }
242 let Registry {
243 urls,
244 mounted_pages,
245 nav_items,
246 searches,
247 mut pages,
248 routes,
249 root,
250 children,
251 ..
252 } = registry;
253 let searches = if global_search {
254 super::search::in_sidebar_order(searches, &nav_items)
255 } else {
256 Vec::new()
257 };
258 if !searches.is_empty() {
259 pages.push(PageFn::new(
260 http::Method::GET,
261 route_path(&format!("{prefix}/{}", super::search::SEARCH_SEGMENT)),
262 super::search::search_page,
263 ));
264 endpoints.push(super::search::shard_path());
265 }
266 let root_redirect = match root {
267 Some(Root::Redirect(target)) => Some(target),
268 Some(Root::Home) | None => None,
269 };
270 let served_paths = served_dirs
271 .iter()
272 .map(|served| served.path.clone())
273 .collect();
274 let signed_roots = served_dirs
275 .iter()
276 .filter(|served| served.signed)
277 .map(|served| served_root(&served.path).to_owned())
278 .collect();
279 let state = Arc::new(PanelState {
280 realm: Arc::new(Realm {
281 prefix: prefix.clone(),
282 auth,
283 login_hint,
284 time_zone,
285 catalog,
286 notifications,
287 }),
288 nav_items,
289 brand,
290 dark_mode: dark_mode.unwrap_or(false),
291 shell_assets,
292 children,
293 mounts,
294 root_redirect: root_redirect.clone(),
295 user_menu,
296 uploads,
297 served_paths,
298 signed_roots,
299 urls,
300 mounted_pages,
301 endpoints,
302 searches,
303 });
304 let prefix_path = route_path(&prefix);
305 builder =
306 builder.layer(topcoat::router::BodyLimit::max(MAX_FORM_BYTES).at(prefix_path.clone()));
307 builder = builder
308 .layer(panel_headers.expect("a policy the header codec rejects fails the mount"));
309 builder = builder.layer(PanelGate::new(Arc::clone(&state.realm)));
311 builder = crate::auth::mount::routes(builder, &prefix, &state.realm.auth);
313 builder = builder.layout(LayoutFn::new(
314 prefix_path.clone(),
315 layout.unwrap_or(Panel::layout_shell),
316 ));
317 builder = builder
320 .page(PageFn::new(
321 http::Method::GET,
322 route_path(&format!("{prefix}/{}", error_page::ERROR_SEGMENT)),
323 error_page::error_page,
324 ))
325 .route(RouteFn::new(
326 Methods::Any,
327 route_path(&format!("{prefix}/{{*rest}}")),
328 error_page::unmatched,
329 ));
330 for served in served_dirs {
331 builder = builder
332 .layer(headers::ServedFileHeaders::new(&served.path, served.signed))
333 .serve_dir(route_path(&served.path), served.dir);
334 }
335 for page in pages {
336 builder = builder.page(page);
337 }
338 for route in routes {
339 builder = builder.route(route);
340 }
341 if root_redirect.is_some() {
342 builder = builder.route(RouteFn::new(
343 http::Method::GET,
344 prefix_path,
345 panel_root_redirect,
346 ));
347 }
348 builder
349 .get_app_context_mut::<Panels>()
350 .expect("the shared panel state is installed above")
351 .0
352 .push(state);
353 Ok(builder)
354 }
355}
356
357impl Registry {
358 fn register_all(
361 &mut self,
362 registrations: Vec<Box<dyn super::register::Registration>>,
363 configuration_errors: Vec<DeclarationError>,
364 ) -> (Arc<Mounts>, Vec<DeclarationError>) {
365 for registration in registrations {
366 registration.register(self);
367 }
368 self.link_relations();
369 let mut errors = configuration_errors;
370 errors.append(&mut self.errors);
371 (Arc::new(std::mem::take(&mut self.mounts)), errors)
372 }
373
374 fn check_all(
377 &self,
378 db: &Db,
379 mounts: &Arc<Mounts>,
380 has_uploader: bool,
381 errors: &mut Vec<DeclarationError>,
382 ) {
383 let cx = validation_cx(db, mounts);
384 for check in &self.page_checks {
385 check(&cx, errors);
386 }
387 for registered in &self.resources {
388 (registered.check)(&cx, errors);
389 if !has_uploader {
391 errors.extend(registered.file_fields.iter().map(|field| DeclarationError {
392 resource: Some(registered.name),
393 site: Site::Form,
394 kind: DeclarationErrorKind::FileFieldWithoutUploader {
395 field: field.clone(),
396 },
397 }));
398 }
399 }
400 }
401}
402
403fn mount_errors(
405 builder: &RouterBuilder,
406 prefix: &str,
407 shell_assets: bool,
408 served_dirs: &[headers::ServedDir],
409) -> Vec<DeclarationError> {
410 let mut errors = Vec::new();
411 let mut refuse = |kind| errors.push(DeclarationError::panel(kind));
412 if shell_assets && builder.get_app_context::<AssetConfig>().is_none() {
413 refuse(DeclarationErrorKind::ShellAssetsWithoutBundle);
414 }
415 if under_prefix(RUNTIME_PREFIX, prefix) || under_prefix(prefix, RUNTIME_PREFIX) {
416 refuse(DeclarationErrorKind::PrefixOverlapsRuntime {
417 prefix: prefix.to_string(),
418 });
419 }
420 for (index, served) in served_dirs.iter().enumerate() {
421 let root = served_root(&served.path);
422 if served_dirs[..index]
423 .iter()
424 .any(|seen| served_root(&seen.path) == root)
425 {
426 refuse(DeclarationErrorKind::ServeDirTwice {
427 path: served.path.clone(),
428 });
429 }
430 }
431 if let Some(panels) = builder.get_app_context::<Panels>() {
432 for other in &panels.0 {
433 for ServedDir { path, .. } in served_dirs {
434 if other
435 .served_paths
436 .iter()
437 .any(|served| served_root(served) == served_root(path))
438 {
439 refuse(DeclarationErrorKind::ServeDirTaken {
440 path: path.clone(),
441 panel: other.realm.prefix.clone(),
442 });
443 }
444 }
445 let other = &other.realm.prefix;
446 if under_prefix(other, prefix) || under_prefix(prefix, other) {
447 refuse(DeclarationErrorKind::PrefixOverlapsPanel {
448 other: other.clone(),
449 });
450 }
451 }
452 }
453 errors
454}
455
456fn install_shared(mut builder: RouterBuilder) -> RouterBuilder {
458 builder = builder.cookies();
459 if builder.get_app_context::<SessionConfig>().is_none() {
460 builder = builder.sessions(SessionConfig::builder().lifetime(SESSION_LIFETIME).build());
461 }
462 builder = builder
463 .layer(RuntimeGate::new())
464 .app_context(Panels::default())
465 .app_context(AuthHost(&PanelHost))
466 .app_context(MountScope(request_mounts))
467 .app_context(TenantSource(crate::auth::session_tenant))
468 .app_context(crate::values::ZoneSource(crate::auth::session_time_zone))
469 .app_context(crate::schema::FileHref(super::url::file))
470 .app_context(crate::i18n::CatalogSource(crate::auth::session_catalog))
471 .app_context(NotificationBus::new());
472 if builder.get_app_context::<RuntimeSetup>().is_none() {
475 builder = builder.runtime();
476 }
477 if builder.get_app_context::<PrefetchMode>().is_none() {
478 builder = builder.prefetch(PrefetchMode::Never);
479 }
480 builder
481}
482
483pub(crate) fn panel_root_redirect(cx: &Cx, _body: Body) -> RouteFuture<'_> {
486 Box::pin(async move {
487 crate::auth::guard(cx)?;
489 let target = current(cx)
490 .and_then(|panel| panel.root_redirect.clone())
491 .ok_or_else(topcoat::router::error::not_found)?;
492 Err(redirect(target).into())
493 })
494}
495
496pub(super) fn is_directory_pattern(path: &str) -> bool {
498 Path::from_str(path)
499 .ok()
500 .and_then(|parsed| parsed.segments().next_back())
501 .is_some_and(|segment| segment.as_catch_all().is_some())
502}
503
504pub(super) fn validate_route_segment(
507 item: &'static str,
508 segment: &str,
509) -> Result<(), DeclarationErrorKind> {
510 match segment_fault(segment) {
511 Some(fault) => Err(DeclarationErrorKind::InvalidSegment {
512 item,
513 segment: segment.to_string(),
514 fault,
515 }),
516 None => Ok(()),
517 }
518}
519
520fn validation_cx(db: &Db, mounts: &Arc<Mounts>) -> Cx {
524 let mut app_context = topcoat::context::AppContext::new();
525 app_context.insert(db.clone());
526 app_context.insert(Arc::clone(mounts));
527 app_context.insert(AuthHost(&PanelHost));
528 app_context.insert(MountScope(|cx| {
529 topcoat::context::try_app_context::<Arc<Mounts>>(cx).map(|mounts| &**mounts)
530 }));
531 Cx::new(Arc::new(app_context))
532}
533
534pub(super) fn served_root(path: &str) -> &str {
537 path.rsplit_once("{*").map_or(path, |(root, _)| root)
538}
539
540#[cfg(test)]
541mod tests;