1use std::sync::Arc;
5
6use toasty::Db;
7use topcoat::{
8 Result,
9 asset::AssetConfig,
10 context::Cx,
11 cookie::CookieLayer,
12 metadata::{Metadata, Robots},
13 router::{
14 Body, LayoutFn, Methods, PageFn, Path, RouteFn, RouteFuture, RouterBuilder,
15 RouterBuilderDirectoryExt, error::redirect,
16 },
17 runtime::{PrefetchMode, RuntimeLayer},
18};
19
20use super::{
21 Panel, Root, error_page,
22 files::served_root,
23 headers::{self, ServedDir},
24 host::PanelHost,
25 register::Registry,
26 shell::{LANGUAGE_SEGMENT, language_post},
27 state::{PanelState, Panels, current, under_prefix},
28};
29use crate::{
30 DeclarationError, DeclarationErrorKind, MountError, Site,
31 auth::{AuthHost, PagePanelStamp, PanelGate, Realm, RuntimeGate},
32 bus::NotificationBus,
33 declaration::segment_fault,
34 naming::route_path,
35 resource::{MountScope, Mounts},
36 tenancy::TenantSource,
37 toasty_compat::model::AppSchema,
38 topcoat_compat::RUNTIME_PREFIX,
39};
40
41mod handle;
42
43pub use self::handle::PanelHandle;
44use self::handle::request_mounts;
45
46pub trait RouterBuilderPanelExt: Sized {
84 fn panel(self, panel: Panel) -> Result<Self>;
99
100 fn panel_handle(&self, prefix: &str) -> Option<PanelHandle>;
104}
105
106impl RouterBuilderPanelExt for RouterBuilder {
107 fn panel(self, panel: Panel) -> Result<Self> {
108 panel.mount(self)
109 }
110
111 fn panel_handle(&self, prefix: &str) -> Option<PanelHandle> {
112 let panel = self
113 .try_context::<Panels>()?
114 .by_prefix(&crate::naming::normalize_prefix(prefix))?;
115 Some(PanelHandle {
116 db: self.try_context::<Db>()?.clone(),
117 mounts: Arc::clone(&panel.mounts),
118 catalog: Arc::clone(&panel.realm.catalog),
119 panel: Some(Arc::clone(panel)),
120 prefix: panel.realm.prefix.clone(),
121 bus: self
122 .try_context::<NotificationBus>()
123 .cloned()
124 .unwrap_or_else(NotificationBus::new),
125 })
126 }
127}
128
129impl Panel {
130 fn mount(self, mut builder: RouterBuilder) -> Result<RouterBuilder> {
131 let db = builder.try_context::<Db>().cloned();
132 let services = crate::auth::mount::Services::of(&builder);
133 let mut registry = Registry::new(self.prefix.clone(), db.as_ref().map(AppSchema::of_db));
134 let Panel {
135 prefix,
136 shell_assets,
137 brand,
138 dark_mode,
139 environment,
140 layout,
141 registrations,
142 content_security_policy,
143 content_security_policy_sources,
144 strict_transport_security,
145 configuration_errors,
146 uploads,
147 served_dirs,
148 login_hint,
149 user_menu,
150 auth,
151 mut endpoints,
152 notifications,
153 time_zone,
154 locales,
155 translations,
156 global_search,
157 limits,
158 commit_hooks,
159 audit_log,
160 } = self;
161 let (mounts, mut errors) =
162 registry.register_all(registrations, audit_log, configuration_errors);
163 let serves_tenants = serves_tenants(&auth, &mounts);
164 errors.extend(registry.unoffered_auth_pages(&auth));
165 errors.extend(mount_errors(
166 &builder,
167 &prefix,
168 shell_assets.is_some(),
169 &served_dirs,
170 ));
171 match &db {
172 Some(db) => {
173 registry.check_all(
174 db,
175 &mounts,
176 serves_tenants,
177 uploads.is_some(),
178 &limits,
179 &mut errors,
180 );
181 let toggles = registry.resources.iter().any(|resource| resource.toggles);
182 errors.extend(
183 crate::auth::mount::declaration_errors(
184 db,
185 &auth,
186 toggles,
187 notifications,
188 services,
189 )
190 .into_iter()
191 .map(DeclarationError::panel),
192 );
193 errors.extend(super::audit::declaration_error(db, ®istry.urls));
194 }
195 None => errors.push(DeclarationError::panel(DeclarationErrorKind::MissingDb)),
196 }
197 let catalog = build_catalog(&locales, &translations, &mut errors);
198 let panel_headers = match content_security_policy
199 .resolve(&content_security_policy_sources)
200 .and_then(|policy| {
201 headers::PanelHeaders::new(
202 prefix.clone(),
203 policy.as_deref(),
204 strict_transport_security,
205 )
206 }) {
207 Ok(layer) => Some(layer),
208 Err(kind) => {
209 errors.push(DeclarationError::panel(kind));
210 None
211 }
212 };
213 if !errors.is_empty() {
214 return Err(MountError::new(&prefix, errors).into());
215 }
216 let catalog = Arc::new(catalog.expect("a catalog that failed to build fails the mount"));
217 if builder.try_context::<Panels>().is_none() {
218 builder = install_shared(builder);
219 }
220 let pages = super::area::settings_paths(&prefix, ®istry.auth_pages);
221 let Registry {
222 urls,
223 mounted_pages,
224 nav_items,
225 areas,
226 searches,
227 pages: mut page_routes,
228 routes,
229 root,
230 children,
231 ..
232 } = registry;
233 let searches = if global_search {
234 super::search::in_sidebar_order(searches, &nav_items)
235 } else {
236 Vec::new()
237 };
238 if !searches.is_empty() {
239 page_routes.push(PageFn::new(
240 http::Method::GET,
241 route_path(&format!("{prefix}/{}", super::search::SEARCH_SEGMENT)),
242 super::search::search_page,
243 ));
244 endpoints.push(super::search::shard_path());
245 }
246 let root_redirect = match root {
247 Some(Root::Redirect(target)) => Some(target),
248 Some(Root::Home) | None => None,
249 };
250 let (served_paths, signed_roots) = super::files::served_paths(&served_dirs);
251 let state = Arc::new(PanelState {
252 realm: Arc::new(Realm {
253 prefix: prefix.clone(),
254 auth,
255 login_hint,
256 time_zone,
257 catalog,
258 notifications,
259 serves_tenants,
260 pages,
261 audit_log: urls.contains_key(&std::any::TypeId::of::<super::AuditLog>()),
262 }),
263 nav_items,
264 areas,
265 brand,
266 dark_mode,
267 environment,
268 shell_assets,
269 children,
270 mounts,
271 root_redirect: root_redirect.clone(),
272 user_menu,
273 uploads,
274 served_paths,
275 signed_roots,
276 urls,
277 mounted_pages,
278 endpoints,
279 searches,
280 limits,
281 commit_hooks,
282 });
283 let prefix_path = route_path(&prefix);
284 builder = builder.layer(limits.body_cap(prefix_path.clone()));
285 builder = builder
286 .layer(panel_headers.expect("a policy the header codec rejects fails the mount"));
287 builder = builder.layer(PanelGate::new(Arc::clone(&state.realm)));
289 builder = crate::auth::mount::routes(builder, &state.realm);
291 if state.realm.catalog.locales().len() > 1 {
293 builder = builder.route(RouteFn::new(
294 http::Method::POST,
295 route_path(&format!("{prefix}/{LANGUAGE_SEGMENT}")),
296 language_post,
297 ));
298 }
299 builder = builder.layout(
301 LayoutFn::new(prefix_path.clone(), layout.unwrap_or(Panel::layout_shell))
302 .with(Metadata::new().robots(Robots::new().index(false).follow(false))),
303 );
304 builder = builder
307 .page(PageFn::new(
308 http::Method::GET,
309 route_path(&format!("{prefix}/{}", error_page::ERROR_SEGMENT)),
310 error_page::error_page,
311 ))
312 .route(RouteFn::new(
313 Methods::Any,
314 route_path(&format!("{prefix}/{{*rest}}")),
315 error_page::unmatched,
316 ));
317 for served in served_dirs {
318 builder = builder
319 .layer(headers::ServedFileHeaders::new(&served.path, served.signed))
320 .serve_dir(route_path(&served.path), served.dir);
321 }
322 for page in page_routes {
323 builder = builder.page(page);
324 }
325 for route in routes {
326 builder = builder.route(route);
327 }
328 if root_redirect.is_some() {
329 builder = builder.route(RouteFn::new(
330 http::Method::GET,
331 prefix_path,
332 panel_root_redirect,
333 ));
334 }
335 let mut panels = builder.context::<Panels>().clone();
337 panels.0.push(state);
338 Ok(builder.with(panels))
339 }
340}
341
342impl Registry {
343 fn register_all(
346 &mut self,
347 registrations: Vec<Box<dyn super::register::Registration>>,
348 audit_log: Option<super::audit::CustomizeAuditLog>,
349 configuration_errors: Vec<DeclarationError>,
350 ) -> (Arc<Mounts>, Vec<DeclarationError>) {
351 if let Some(customize) = audit_log {
352 self.audit_log = super::register::AuditLogList::Unplaced(customize);
353 }
354 for registration in registrations {
355 registration.register(self);
356 }
357 self.place_unplaced_audit_log();
358 self.link_relations();
359 self.check_root();
360 let mut errors = configuration_errors;
361 errors.append(&mut self.errors);
362 (Arc::new(std::mem::take(&mut self.mounts)), errors)
363 }
364
365 fn check_all(
368 &self,
369 db: &Db,
370 mounts: &Arc<Mounts>,
371 serves_tenants: bool,
372 has_uploader: bool,
373 limits: &crate::Limits,
374 errors: &mut Vec<DeclarationError>,
375 ) {
376 let mut cx = validation_cx(db, mounts);
377 if serves_tenants {
378 cx = cx.with(crate::resource::ServesTenants);
379 }
380 for check in &self.page_checks {
381 check(&cx, errors);
382 }
383 for registered in &self.resources {
384 (registered.check)(&cx, errors);
385 if !has_uploader {
387 errors.extend(registered.file_fields.iter().map(|field| DeclarationError {
388 resource: Some(registered.name),
389 site: Site::Form,
390 kind: DeclarationErrorKind::FileFieldWithoutUploader {
391 field: field.clone(),
392 },
393 }));
394 }
395 let cap = limits.body_bytes;
397 errors.extend(
398 registered
399 .file_caps
400 .iter()
401 .filter(|(_, max)| *max >= cap)
402 .map(|(field, max)| DeclarationError {
403 resource: Some(registered.name),
404 site: Site::Form,
405 kind: DeclarationErrorKind::FieldRule {
406 field: field.clone(),
407 fault: crate::FieldRuleFault::AboveFormCap { max: *max, cap },
408 },
409 }),
410 );
411 }
412 }
413}
414
415fn serves_tenants(auth: &crate::auth::AuthConfig, mounts: &Mounts) -> bool {
418 auth.tenant_registration_page().is_some()
419 || auth.tenant_editor().is_some()
420 || mounts.has_tenant_models()
421}
422
423fn mount_errors(
425 builder: &RouterBuilder,
426 prefix: &str,
427 shell_assets: bool,
428 served_dirs: &[headers::ServedDir],
429) -> Vec<DeclarationError> {
430 let mut errors = Vec::new();
431 let mut refuse = |kind| errors.push(DeclarationError::panel(kind));
432 if shell_assets && builder.try_context::<AssetConfig>().is_none() {
433 refuse(DeclarationErrorKind::ShellAssetsWithoutBundle);
434 }
435 if under_prefix(RUNTIME_PREFIX, prefix) || under_prefix(prefix, RUNTIME_PREFIX) {
436 refuse(DeclarationErrorKind::PrefixOverlapsRuntime {
437 prefix: prefix.to_string(),
438 });
439 }
440 for (index, served) in served_dirs.iter().enumerate() {
441 let root = served_root(&served.path);
442 if served_dirs[..index]
443 .iter()
444 .any(|seen| served_root(&seen.path) == root)
445 {
446 refuse(DeclarationErrorKind::ServeDirTwice {
447 path: served.path.clone(),
448 });
449 }
450 }
451 if let Some(panels) = builder.try_context::<Panels>() {
452 for other in &panels.0 {
453 for ServedDir { path, .. } in served_dirs {
454 if other
455 .served_paths
456 .iter()
457 .any(|served| served_root(served) == served_root(path))
458 {
459 refuse(DeclarationErrorKind::ServeDirTaken {
460 path: path.clone(),
461 panel: other.realm.prefix.clone(),
462 });
463 }
464 }
465 let other = &other.realm.prefix;
466 if under_prefix(other, prefix) || under_prefix(prefix, other) {
467 refuse(DeclarationErrorKind::PrefixOverlapsPanel {
468 other: other.clone(),
469 });
470 }
471 }
472 }
473 errors
474}
475
476fn install_shared(mut builder: RouterBuilder) -> RouterBuilder {
478 builder = builder
479 .layer(CookieLayer::new())
480 .layer(RuntimeGate::new())
481 .with(Panels::default())
482 .with(AuthHost(&PanelHost))
483 .with(MountScope(request_mounts))
484 .with(TenantSource(crate::auth::session_tenant))
485 .with(crate::values::ZoneSource(crate::auth::session_time_zone))
486 .with(crate::schema::FileHref(super::url::file))
487 .with(crate::page::PageBody(super::pages::declared_body))
488 .with(crate::i18n::CatalogSource(crate::auth::session_catalog))
489 .with(NotificationBus::new());
490 builder = builder.layer(RuntimeLayer::new());
494 if builder.try_context::<PrefetchMode>().is_none() {
495 builder = builder.with(PrefetchMode::Never);
496 }
497 builder.layer(PagePanelStamp)
500}
501
502pub(crate) fn panel_root_redirect(cx: &Cx, _body: Body) -> RouteFuture<'_> {
505 Box::pin(async move {
506 crate::auth::guard(cx)?;
508 let target = current(cx)
509 .and_then(|panel| panel.root_redirect.clone())
510 .ok_or_else(topcoat::router::error::not_found)?;
511 Err(redirect(target).into())
512 })
513}
514
515pub(super) fn is_directory_pattern(path: &str) -> bool {
517 Path::from_str(path)
518 .ok()
519 .and_then(|parsed| parsed.segments().next_back())
520 .is_some_and(|segment| segment.as_catch_all().is_some())
521}
522
523pub(super) fn validate_route_segment(
526 item: &'static str,
527 segment: &str,
528) -> Result<(), DeclarationErrorKind> {
529 match segment_fault(segment) {
530 Some(fault) => Err(DeclarationErrorKind::InvalidSegment {
531 item,
532 segment: segment.to_string(),
533 fault,
534 }),
535 None => Ok(()),
536 }
537}
538
539fn build_catalog(
542 locales: &[topcoat::l10n::Locale],
543 translations: &[(topcoat::l10n::Locale, String)],
544 errors: &mut Vec<DeclarationError>,
545) -> Option<crate::i18n::Catalog> {
546 crate::i18n::Catalog::new(locales, translations)
547 .map_err(|faults| {
548 errors.extend(
549 faults
550 .into_iter()
551 .map(|fault| DeclarationError::panel(DeclarationErrorKind::Locale(fault))),
552 );
553 })
554 .ok()
555}
556
557fn validation_cx(db: &Db, mounts: &Arc<Mounts>) -> Cx {
561 let mut values = topcoat::context::ContextMap::new();
562 values.insert(db.clone());
563 values.insert(Arc::clone(mounts));
564 values.insert(AuthHost(&PanelHost));
565 values.insert(MountScope(|cx| {
566 topcoat::context::try_context::<Arc<Mounts>>(cx).map(|mounts| &**mounts)
567 }));
568 Cx::new().with_shared(Arc::new(values))
569}
570
571#[cfg(test)]
572mod tests;