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 banners,
151 auth,
152 mut endpoints,
153 notifications,
154 time_zone,
155 locales,
156 translations,
157 global_search,
158 limits,
159 commit_hooks,
160 audit_log,
161 } = self;
162 let (mounts, mut errors) =
163 registry.register_all(registrations, audit_log, configuration_errors);
164 let serves_tenants = serves_tenants(&auth, &mounts);
165 errors.extend(registry.unoffered_auth_pages(&auth));
166 errors.extend(mount_errors(
167 &builder,
168 &prefix,
169 shell_assets.is_some(),
170 &served_dirs,
171 ));
172 match &db {
173 Some(db) => {
174 registry.check_all(
175 db,
176 &mounts,
177 serves_tenants,
178 uploads.is_some(),
179 &limits,
180 &mut errors,
181 );
182 let toggles = registry.resources.iter().any(|resource| resource.toggles);
183 errors.extend(
184 crate::auth::mount::declaration_errors(
185 db,
186 &auth,
187 toggles,
188 notifications,
189 services,
190 )
191 .into_iter()
192 .map(DeclarationError::panel),
193 );
194 errors.extend(super::audit::declaration_error(db, ®istry.urls));
195 }
196 None => errors.push(DeclarationError::panel(DeclarationErrorKind::MissingDb)),
197 }
198 let catalog = build_catalog(&locales, &translations, &mut errors);
199 let panel_headers = match content_security_policy
200 .resolve(&content_security_policy_sources)
201 .and_then(|policy| {
202 headers::PanelHeaders::new(
203 prefix.clone(),
204 policy.as_deref(),
205 strict_transport_security,
206 )
207 }) {
208 Ok(layer) => Some(layer),
209 Err(kind) => {
210 errors.push(DeclarationError::panel(kind));
211 None
212 }
213 };
214 if !errors.is_empty() {
215 return Err(MountError::new(&prefix, errors).into());
216 }
217 let catalog = Arc::new(catalog.expect("a catalog that failed to build fails the mount"));
218 if builder.try_context::<Panels>().is_none() {
219 builder = install_shared(builder);
220 }
221 let pages = super::area::settings_paths(&prefix, ®istry.auth_pages);
222 let Registry {
223 urls,
224 mounted_pages,
225 nav_items,
226 areas,
227 searches,
228 pages: mut page_routes,
229 routes,
230 root,
231 children,
232 ..
233 } = registry;
234 let searches = if global_search {
235 super::search::in_sidebar_order(searches, &nav_items)
236 } else {
237 Vec::new()
238 };
239 if !searches.is_empty() {
240 page_routes.push(PageFn::new(
241 http::Method::GET,
242 route_path(&format!("{prefix}/{}", super::search::SEARCH_SEGMENT)),
243 super::search::search_page,
244 ));
245 endpoints.push(super::search::shard_path());
246 }
247 let root_redirect = match root {
248 Some(Root::Redirect(target)) => Some(target),
249 Some(Root::Home) | None => None,
250 };
251 let (served_paths, signed_roots) = super::files::served_paths(&served_dirs);
252 let state = Arc::new(PanelState {
253 realm: Arc::new(Realm {
254 prefix: prefix.clone(),
255 auth,
256 login_hint,
257 time_zone,
258 catalog,
259 notifications,
260 serves_tenants,
261 pages,
262 audit_log: urls.contains_key(&std::any::TypeId::of::<super::AuditLog>()),
263 }),
264 nav_items,
265 areas,
266 brand,
267 dark_mode,
268 environment,
269 shell_assets,
270 children,
271 mounts,
272 root_redirect: root_redirect.clone(),
273 user_menu,
274 banners,
275 uploads,
276 served_paths,
277 signed_roots,
278 urls,
279 mounted_pages,
280 endpoints,
281 searches,
282 limits,
283 commit_hooks,
284 });
285 let prefix_path = route_path(&prefix);
286 builder = builder.layer(limits.body_cap(prefix_path.clone()));
287 builder = builder
288 .layer(panel_headers.expect("a policy the header codec rejects fails the mount"));
289 builder = builder.layer(PanelGate::new(Arc::clone(&state.realm)));
291 builder = crate::auth::mount::routes(builder, &state.realm);
293 if state.realm.catalog.locales().len() > 1 {
295 builder = builder.route(RouteFn::new(
296 http::Method::POST,
297 route_path(&format!("{prefix}/{LANGUAGE_SEGMENT}")),
298 language_post,
299 ));
300 }
301 builder = builder.layout(
303 LayoutFn::new(prefix_path.clone(), layout.unwrap_or(Panel::layout_shell))
304 .with(Metadata::new().robots(Robots::new().index(false).follow(false))),
305 );
306 builder = builder
309 .page(PageFn::new(
310 http::Method::GET,
311 route_path(&format!("{prefix}/{}", error_page::ERROR_SEGMENT)),
312 error_page::error_page,
313 ))
314 .route(RouteFn::new(
315 Methods::Any,
316 route_path(&format!("{prefix}/{{*rest}}")),
317 error_page::unmatched,
318 ));
319 for served in served_dirs {
320 builder = builder
321 .layer(headers::ServedFileHeaders::new(&served.path, served.signed))
322 .serve_dir(route_path(&served.path), served.dir);
323 }
324 for page in page_routes {
325 builder = builder.page(page);
326 }
327 for route in routes {
328 builder = builder.route(route);
329 }
330 if root_redirect.is_some() {
331 builder = builder.route(RouteFn::new(
332 http::Method::GET,
333 prefix_path,
334 panel_root_redirect,
335 ));
336 }
337 let mut panels = builder.context::<Panels>().clone();
339 panels.0.push(state);
340 Ok(builder.with(panels))
341 }
342}
343
344impl Registry {
345 fn register_all(
348 &mut self,
349 registrations: Vec<Box<dyn super::register::Registration>>,
350 audit_log: Option<super::audit::CustomizeAuditLog>,
351 configuration_errors: Vec<DeclarationError>,
352 ) -> (Arc<Mounts>, Vec<DeclarationError>) {
353 if let Some(customize) = audit_log {
354 self.audit_log = super::register::AuditLogList::Unplaced(customize);
355 }
356 for registration in registrations {
357 registration.register(self);
358 }
359 self.place_unplaced_audit_log();
360 self.link_relations();
361 self.check_root();
362 let mut errors = configuration_errors;
363 errors.append(&mut self.errors);
364 (Arc::new(std::mem::take(&mut self.mounts)), errors)
365 }
366
367 fn check_all(
370 &self,
371 db: &Db,
372 mounts: &Arc<Mounts>,
373 serves_tenants: bool,
374 has_uploader: bool,
375 limits: &crate::Limits,
376 errors: &mut Vec<DeclarationError>,
377 ) {
378 let mut cx = validation_cx(db, mounts);
379 if serves_tenants {
380 cx = cx.with(crate::resource::ServesTenants);
381 }
382 for check in &self.page_checks {
383 check(&cx, errors);
384 }
385 for registered in &self.resources {
386 (registered.check)(&cx, errors);
387 if !has_uploader {
389 errors.extend(registered.file_fields.iter().map(|field| DeclarationError {
390 resource: Some(registered.name),
391 site: Site::Form,
392 kind: DeclarationErrorKind::FileFieldWithoutUploader {
393 field: field.clone(),
394 },
395 }));
396 }
397 let cap = limits.body_bytes;
399 errors.extend(
400 registered
401 .file_caps
402 .iter()
403 .filter(|(_, max)| *max >= cap)
404 .map(|(field, max)| DeclarationError {
405 resource: Some(registered.name),
406 site: Site::Form,
407 kind: DeclarationErrorKind::FieldRule {
408 field: field.clone(),
409 fault: crate::FieldRuleFault::AboveFormCap { max: *max, cap },
410 },
411 }),
412 );
413 }
414 }
415}
416
417fn serves_tenants(auth: &crate::auth::AuthConfig, mounts: &Mounts) -> bool {
420 auth.tenant_registration_page().is_some()
421 || auth.tenant_editor().is_some()
422 || mounts.has_tenant_models()
423}
424
425fn mount_errors(
427 builder: &RouterBuilder,
428 prefix: &str,
429 shell_assets: bool,
430 served_dirs: &[headers::ServedDir],
431) -> Vec<DeclarationError> {
432 let mut errors = Vec::new();
433 let mut refuse = |kind| errors.push(DeclarationError::panel(kind));
434 if shell_assets && builder.try_context::<AssetConfig>().is_none() {
435 refuse(DeclarationErrorKind::ShellAssetsWithoutBundle);
436 }
437 if under_prefix(RUNTIME_PREFIX, prefix) || under_prefix(prefix, RUNTIME_PREFIX) {
438 refuse(DeclarationErrorKind::PrefixOverlapsRuntime {
439 prefix: prefix.to_string(),
440 });
441 }
442 for (index, served) in served_dirs.iter().enumerate() {
443 let root = served_root(&served.path);
444 if served_dirs[..index]
445 .iter()
446 .any(|seen| served_root(&seen.path) == root)
447 {
448 refuse(DeclarationErrorKind::ServeDirTwice {
449 path: served.path.clone(),
450 });
451 }
452 }
453 if let Some(panels) = builder.try_context::<Panels>() {
454 for other in &panels.0 {
455 for ServedDir { path, .. } in served_dirs {
456 if other
457 .served_paths
458 .iter()
459 .any(|served| served_root(served) == served_root(path))
460 {
461 refuse(DeclarationErrorKind::ServeDirTaken {
462 path: path.clone(),
463 panel: other.realm.prefix.clone(),
464 });
465 }
466 }
467 let other = &other.realm.prefix;
468 if under_prefix(other, prefix) || under_prefix(prefix, other) {
469 refuse(DeclarationErrorKind::PrefixOverlapsPanel {
470 other: other.clone(),
471 });
472 }
473 }
474 }
475 errors
476}
477
478fn install_shared(mut builder: RouterBuilder) -> RouterBuilder {
480 builder = builder
481 .layer(CookieLayer::new())
482 .layer(RuntimeGate::new())
483 .with(Panels::default())
484 .with(AuthHost(&PanelHost))
485 .with(MountScope(request_mounts))
486 .with(TenantSource(crate::auth::session_tenant))
487 .with(crate::values::ZoneSource(crate::auth::session_time_zone))
488 .with(crate::schema::FileHref(super::url::file))
489 .with(crate::page::PageBody(super::pages::declared_body))
490 .with(crate::i18n::CatalogSource(crate::auth::session_catalog))
491 .with(NotificationBus::new());
492 builder = builder.layer(RuntimeLayer::new());
496 if builder.try_context::<PrefetchMode>().is_none() {
497 builder = builder.with(PrefetchMode::Never);
498 }
499 builder.layer(PagePanelStamp)
502}
503
504pub(crate) fn panel_root_redirect(cx: &Cx, _body: Body) -> RouteFuture<'_> {
507 Box::pin(async move {
508 crate::auth::guard(cx)?;
510 let target = current(cx)
511 .and_then(|panel| panel.root_redirect.clone())
512 .ok_or_else(topcoat::router::error::not_found)?;
513 Err(redirect(target).into())
514 })
515}
516
517pub(super) fn is_directory_pattern(path: &str) -> bool {
519 Path::from_str(path)
520 .ok()
521 .and_then(|parsed| parsed.segments().next_back())
522 .is_some_and(|segment| segment.as_catch_all().is_some())
523}
524
525pub(super) fn validate_route_segment(
528 item: &'static str,
529 segment: &str,
530) -> Result<(), DeclarationErrorKind> {
531 match segment_fault(segment) {
532 Some(fault) => Err(DeclarationErrorKind::InvalidSegment {
533 item,
534 segment: segment.to_string(),
535 fault,
536 }),
537 None => Ok(()),
538 }
539}
540
541fn build_catalog(
544 locales: &[topcoat::l10n::Locale],
545 translations: &[(topcoat::l10n::Locale, String)],
546 errors: &mut Vec<DeclarationError>,
547) -> Option<crate::i18n::Catalog> {
548 crate::i18n::Catalog::new(locales, translations)
549 .map_err(|faults| {
550 errors.extend(
551 faults
552 .into_iter()
553 .map(|fault| DeclarationError::panel(DeclarationErrorKind::Locale(fault))),
554 );
555 })
556 .ok()
557}
558
559fn validation_cx(db: &Db, mounts: &Arc<Mounts>) -> Cx {
563 let mut values = topcoat::context::ContextMap::new();
564 values.insert(db.clone());
565 values.insert(Arc::clone(mounts));
566 values.insert(AuthHost(&PanelHost));
567 values.insert(MountScope(|cx| {
568 topcoat::context::try_context::<Arc<Mounts>>(cx).map(|mounts| &**mounts)
569 }));
570 Cx::new().with_shared(Arc::new(values))
571}
572
573#[cfg(test)]
574mod tests;