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, Path, RouteFn, RouteFuture, RouterBuilder, RouterBuilderDirectoryExt,
14 error::redirect,
15 },
16 runtime::{PrefetchMode, RouterBuilderRuntimeExt, RuntimeSetup},
17 session::{RouterBuilderSessionExt, SessionConfig},
18};
19
20use super::{
21 Panel, Root,
22 forms::MAX_FORM_BYTES,
23 headers,
24 register::Registry,
25 state::{PanelState, Panels, current, under_prefix},
26};
27use crate::{
28 ActionInputFault, DeclarationError, DeclarationErrorKind, ManyToManyFault, MountError, Page,
29 Site,
30 auth::{PanelGate, RuntimeGate, SESSION_LIFETIME},
31 declaration::segment_fault,
32 form::RecordForm,
33 page::MountedPage,
34 policy::Ability,
35 resource::{InputSpec, MountScope, Mounted, Mounts, Resource, require_mounted},
36 tenancy::TenantSource,
37 toasty_compat::{
38 join::JoinTable,
39 model::{self, AppSchema},
40 },
41 topcoat_compat::RUNTIME_PREFIX,
42};
43
44pub trait RouterBuilderPanelExt: Sized {
81 fn panel(self, panel: Panel) -> Result<Self>;
83
84 fn panel_handle(&self, prefix: &str) -> Option<PanelHandle>;
88}
89
90impl RouterBuilderPanelExt for RouterBuilder {
91 fn panel(self, panel: Panel) -> Result<Self> {
92 panel.mount(self)
93 }
94
95 fn panel_handle(&self, prefix: &str) -> Option<PanelHandle> {
96 let panel = self
97 .get_app_context::<Panels>()?
98 .by_prefix(&super::normalize_prefix(prefix))?;
99 Some(PanelHandle {
100 db: self.get_app_context::<Db>()?.clone(),
101 mounts: Arc::clone(&panel.mounts),
102 })
103 }
104}
105
106#[derive(Clone)]
134pub struct PanelHandle {
135 db: Db,
136 mounts: Arc<Mounts>,
137}
138
139impl PanelHandle {
140 pub fn context(&self) -> Cx {
149 validation_cx(&self.db, &self.mounts)
150 }
151}
152
153impl Panel {
154 pub fn handle(self, db: &Db) -> Result<PanelHandle> {
163 let Panel {
164 prefix,
165 registrations,
166 configuration_errors,
167 uploads,
168 ..
169 } = self;
170 let mut registry = Registry::new(prefix.clone(), Some(AppSchema::of_db(db)));
171 let (mounts, mut errors) = registry.register_all(registrations, configuration_errors);
172 registry.check_all(db, &mounts, uploads.is_some(), &mut errors);
173 if !errors.is_empty() {
174 return Err(MountError::new(&prefix, errors).into());
175 }
176 Ok(PanelHandle {
177 db: db.clone(),
178 mounts,
179 })
180 }
181
182 fn mount(self, mut builder: RouterBuilder) -> Result<RouterBuilder> {
183 let db = builder.get_app_context::<Db>().cloned();
184 let mut registry = Registry::new(self.prefix.clone(), db.as_ref().map(AppSchema::of_db));
185 let Panel {
186 prefix,
187 shell_assets,
188 brand,
189 dark_mode,
190 layout,
191 registrations,
192 frame_ancestors,
193 configuration_errors,
194 uploads,
195 served_dirs,
196 login_hint,
197 auth,
198 } = self;
199 let (mounts, mut errors) = registry.register_all(registrations, configuration_errors);
200 errors.extend(mount_errors(
201 &builder,
202 &prefix,
203 shell_assets.is_some(),
204 &served_dirs,
205 ));
206 match &db {
207 Some(db) => {
208 registry.check_all(db, &mounts, uploads.is_some(), &mut errors);
209 if let Err(kind) = crate::auth::check_models_registered(db, &auth) {
210 errors.push(DeclarationError::panel(kind));
211 }
212 errors.extend(
213 crate::auth::check_registration(&auth)
214 .into_iter()
215 .map(DeclarationError::panel),
216 );
217 }
218 None => errors.push(DeclarationError::panel(DeclarationErrorKind::MissingDb)),
219 }
220 if !errors.is_empty() {
221 return Err(MountError::new(&prefix, errors).into());
222 }
223 if builder.get_app_context::<Panels>().is_none() {
224 builder = install_shared(builder);
225 }
226 let Registry {
227 urls,
228 mounted_pages,
229 nav_items,
230 pages,
231 routes,
232 root,
233 children,
234 ..
235 } = registry;
236 let root_redirect = match root {
237 Some(Root::Redirect(target)) => Some(target),
238 Some(Root::Home) | None => None,
239 };
240 let served_paths = served_dirs.iter().map(|(path, _)| path.clone()).collect();
241 let state = Arc::new(PanelState {
242 prefix: prefix.clone(),
243 nav_items,
244 brand,
245 dark_mode: dark_mode.unwrap_or(false),
246 shell_assets,
247 children,
248 mounts,
249 root_redirect: root_redirect.clone(),
250 auth,
251 login_hint,
252 uploads,
253 served_paths,
254 urls,
255 mounted_pages,
256 });
257 let prefix_path = route_path(&prefix);
258 builder =
259 builder.layer(topcoat::router::BodyLimit::max(MAX_FORM_BYTES).at(prefix_path.clone()));
260 if let Some(directive) = frame_ancestors {
261 builder = builder.layer(headers::FrameAncestors::new(directive, prefix.clone()));
262 }
263 builder = builder.layer(PanelGate::new(Arc::clone(&state)));
265 if state.gates() {
267 let login_path = route_path(&format!("{prefix}/login"));
268 let logout_path = route_path(&format!("{prefix}/logout"));
269 let tenant_path = route_path(&format!("{prefix}/tenant"));
270 builder = builder
271 .layer(
272 topcoat::router::BodyLimit::max(crate::auth::MAX_LOGIN_BYTES)
273 .at(login_path.clone()),
274 )
275 .route(RouteFn::new(
276 http::Method::GET,
277 login_path.clone(),
278 crate::auth::login_page,
279 ))
280 .route(RouteFn::new(
281 http::Method::POST,
282 login_path,
283 crate::auth::login_post,
284 ))
285 .route(RouteFn::new(
286 http::Method::POST,
287 logout_path,
288 crate::auth::logout_post,
289 ))
290 .route(RouteFn::new(
291 http::Method::POST,
292 tenant_path,
293 crate::auth::tenant_post,
294 ));
295 }
296 if state.auth.registrar().is_some() {
297 let register_path = route_path(&format!("{prefix}/register"));
298 builder = builder
299 .layer(
300 topcoat::router::BodyLimit::max(crate::auth::MAX_LOGIN_BYTES)
301 .at(register_path.clone()),
302 )
303 .route(RouteFn::new(
304 http::Method::GET,
305 register_path.clone(),
306 crate::auth::register_page,
307 ))
308 .route(RouteFn::new(
309 http::Method::POST,
310 register_path,
311 crate::auth::register_post,
312 ));
313 }
314 builder = builder.layout(LayoutFn::new(
315 prefix_path.clone(),
316 layout.unwrap_or(Panel::layout_shell),
317 ));
318 for (path, dir) in served_dirs {
319 builder = builder
320 .layer(headers::ServedFileHeaders::new(&path))
321 .serve_dir(route_path(&path), dir);
322 }
323 for page in pages {
324 builder = builder.page(page);
325 }
326 for route in routes {
327 builder = builder.route(route);
328 }
329 if root_redirect.is_some() {
330 builder = builder.route(RouteFn::new(
331 http::Method::GET,
332 prefix_path,
333 panel_root_redirect,
334 ));
335 }
336 builder
337 .get_app_context_mut::<Panels>()
338 .expect("the shared panel state is installed above")
339 .0
340 .push(state);
341 Ok(builder)
342 }
343}
344
345impl Registry {
346 fn register_all(
349 &mut self,
350 registrations: Vec<Box<dyn super::register::Registration>>,
351 configuration_errors: Vec<DeclarationError>,
352 ) -> (Arc<Mounts>, Vec<DeclarationError>) {
353 for registration in registrations {
354 registration.register(self);
355 }
356 self.link_relations();
357 let mut errors = configuration_errors;
358 errors.append(&mut self.errors);
359 (Arc::new(std::mem::take(&mut self.mounts)), errors)
360 }
361
362 fn check_all(
365 &self,
366 db: &Db,
367 mounts: &Arc<Mounts>,
368 has_uploader: bool,
369 errors: &mut Vec<DeclarationError>,
370 ) {
371 let cx = validation_cx(db, mounts);
372 for check in &self.page_checks {
373 check(&cx, errors);
374 }
375 for registered in &self.resources {
376 (registered.check)(&cx, errors);
377 if !has_uploader {
379 errors.extend(registered.file_fields.iter().map(|field| DeclarationError {
380 resource: Some(registered.name),
381 site: Site::Form,
382 kind: DeclarationErrorKind::FileFieldWithoutUploader {
383 field: field.clone(),
384 },
385 }));
386 }
387 }
388 }
389}
390
391fn mount_errors(
393 builder: &RouterBuilder,
394 prefix: &str,
395 shell_assets: bool,
396 served_dirs: &[(String, std::path::PathBuf)],
397) -> Vec<DeclarationError> {
398 let mut errors = Vec::new();
399 let mut refuse = |kind| errors.push(DeclarationError::panel(kind));
400 if shell_assets && builder.get_app_context::<AssetConfig>().is_none() {
401 refuse(DeclarationErrorKind::ShellAssetsWithoutBundle);
402 }
403 if under_prefix(RUNTIME_PREFIX, prefix) || under_prefix(prefix, RUNTIME_PREFIX) {
404 refuse(DeclarationErrorKind::PrefixOverlapsRuntime {
405 prefix: prefix.to_string(),
406 });
407 }
408 for (index, (path, _)) in served_dirs.iter().enumerate() {
409 let root = served_root(path);
410 if served_dirs[..index]
411 .iter()
412 .any(|(seen, _)| served_root(seen) == root)
413 {
414 refuse(DeclarationErrorKind::ServeDirTwice { path: path.clone() });
415 }
416 }
417 if let Some(panels) = builder.get_app_context::<Panels>() {
418 for other in &panels.0 {
419 for (path, _) in served_dirs {
420 if other
421 .served_paths
422 .iter()
423 .any(|served| served_root(served) == served_root(path))
424 {
425 refuse(DeclarationErrorKind::ServeDirTaken {
426 path: path.clone(),
427 panel: other.prefix.clone(),
428 });
429 }
430 }
431 if under_prefix(&other.prefix, prefix) || under_prefix(prefix, &other.prefix) {
432 refuse(DeclarationErrorKind::PrefixOverlapsPanel {
433 other: other.prefix.clone(),
434 });
435 }
436 }
437 }
438 errors
439}
440
441fn install_shared(mut builder: RouterBuilder) -> RouterBuilder {
443 builder = builder.cookies();
444 if builder.get_app_context::<SessionConfig>().is_none() {
445 builder = builder.sessions(SessionConfig::builder().lifetime(SESSION_LIFETIME).build());
446 }
447 builder = builder
448 .layer(RuntimeGate::new())
449 .app_context(Panels::default())
450 .app_context(MountScope(|cx| current(cx).map(|panel| &*panel.mounts)))
451 .app_context(TenantSource(crate::auth::session_tenant));
452 if builder.get_app_context::<RuntimeSetup>().is_none() {
455 builder = builder.runtime();
456 }
457 if builder.get_app_context::<PrefetchMode>().is_none() {
458 builder = builder.prefetch(PrefetchMode::Never);
459 }
460 builder
461}
462
463pub(crate) fn panel_root_redirect(cx: &Cx, _body: Body) -> RouteFuture<'_> {
466 Box::pin(async move {
467 crate::auth::guard(cx)?;
469 let target = current(cx)
470 .and_then(|panel| panel.root_redirect.clone())
471 .ok_or_else(topcoat::router::error::not_found)?;
472 Err(redirect(target).into())
473 })
474}
475
476pub(super) fn is_directory_pattern(path: &str) -> bool {
478 Path::from_str(path)
479 .ok()
480 .and_then(|parsed| parsed.segments().next_back())
481 .is_some_and(|segment| segment.as_catch_all().is_some())
482}
483
484pub(super) fn validate_route_segment(
487 item: &'static str,
488 segment: &str,
489) -> Result<(), DeclarationErrorKind> {
490 match segment_fault(segment) {
491 Some(fault) => Err(DeclarationErrorKind::InvalidSegment {
492 item,
493 segment: segment.to_string(),
494 fault,
495 }),
496 None => Ok(()),
497 }
498}
499
500pub(super) type ResourceCheck = fn(&Cx, &mut Vec<DeclarationError>);
503
504pub(super) fn check_resource<R: Resource>(cx: &Cx, errors: &mut Vec<DeclarationError>) {
506 let Ok(declared) = require_mounted::<R>(cx) else {
507 return;
508 };
509 let tenancy = &declared.tenancy;
510 if let Some(Err(kind)) = tenancy.column_field() {
511 errors.push(DeclarationError::of::<R>(Site::Tenancy, kind));
512 }
513 if tenancy.via_is_single() == Some(true) {
514 errors.push(DeclarationError::of::<R>(
515 Site::Tenancy,
516 DeclarationErrorKind::TenancyViaOwnColumn,
517 ));
518 }
519 let mut form_errors = declared.form.declaration_errors();
520 form_errors.extend(declared.form.empty_choices());
521 let form_is_sound = form_errors.is_empty();
522 for (site, kinds) in [
523 (Site::Table, declared.table.declaration_errors()),
524 (Site::Form, form_errors),
525 (Site::Detail, declared.detail.declaration_errors()),
526 ] {
527 errors.extend(
528 kinds
529 .into_iter()
530 .map(|kind| DeclarationError::of::<R>(site.clone(), kind)),
531 );
532 }
533 for (site, kinds) in [
534 (Site::Table, declared.table.unavailable_sources(cx)),
535 (Site::Detail, declared.detail.unavailable_sources(cx)),
536 ] {
537 errors.extend(
538 kinds
539 .into_iter()
540 .map(|kind| DeclarationError::of::<R>(site.clone(), kind)),
541 );
542 }
543 check_actions(cx, &declared, errors);
544 check_form_declaration(cx, &declared, form_is_sound, errors);
545}
546
547fn check_actions<R: Resource>(cx: &Cx, declared: &Mounted<R>, errors: &mut Vec<DeclarationError>) {
552 let actions = declared
553 .actions
554 .entries()
555 .iter()
556 .map(|action| (action.name, action.input))
557 .chain(
558 declared
559 .header_actions
560 .entries()
561 .iter()
562 .map(|action| (action.name, action.input)),
563 );
564 check_inputs::<R>(cx, actions, errors);
565}
566
567pub(super) type PageCheck = Box<dyn Fn(&Cx, &mut Vec<DeclarationError>) + Send>;
569
570pub(super) fn check_page<P: Page>(page: Arc<MountedPage>) -> PageCheck {
572 Box::new(move |cx, errors| {
573 check_inputs::<P>(
574 cx,
575 page.header_actions
576 .entries()
577 .iter()
578 .map(|action| (action.name, action.input)),
579 errors,
580 );
581 })
582}
583
584fn check_inputs<T: 'static>(
588 cx: &Cx,
589 actions: impl Iterator<Item = (&'static str, InputSpec)>,
590 errors: &mut Vec<DeclarationError>,
591) {
592 let mut seen = std::collections::HashSet::new();
593 for (name, spec) in actions {
594 if !seen.insert(name) {
595 errors.push(DeclarationError::of::<T>(
596 Site::Registration,
597 DeclarationErrorKind::DuplicateAction { name },
598 ));
599 }
600 let input = (spec.schema)();
601 let mut kinds = input.declaration_errors();
602 kinds.extend(input.empty_choices());
603 let mut faults: Vec<ActionInputFault> = input
604 .fields()
605 .filter_map(|field| {
606 let name = field.name().to_string();
607 if crate::resource::RESERVED_KEYS.contains(&name.as_str()) {
608 Some(ActionInputFault::ReservedField(name))
609 } else if field.is_file() {
610 Some(ActionInputFault::FileField(name))
611 } else if field.parent_key().is_some() {
612 Some(ActionInputFault::DependentChoice(name))
613 } else {
614 None
615 }
616 })
617 .collect();
618 if !spec.takes_input && (spec.parse)(cx, &crate::form::FormValues::new()).is_err() {
619 faults.push(ActionInputFault::RefusesEmpty);
620 }
621 kinds.extend(
622 faults
623 .into_iter()
624 .map(|fault| DeclarationErrorKind::ActionInput {
625 action: name,
626 fault,
627 }),
628 );
629 errors.extend(
630 kinds
631 .into_iter()
632 .map(|kind| DeclarationError::of::<T>(Site::Registration, kind)),
633 );
634 }
635}
636
637fn form_error<R: Resource>(kind: DeclarationErrorKind) -> DeclarationError {
639 DeclarationError::of::<R>(Site::Form, kind)
640}
641
642fn check_form_declaration<R: Resource>(
644 cx: &Cx,
645 declared: &Mounted<R>,
646 form_is_sound: bool,
647 errors: &mut Vec<DeclarationError>,
648) {
649 if !form_is_sound {
651 return;
652 }
653 if <R::Form as RecordForm>::HAS_FORM {
654 check_form_inner(cx, declared, errors);
655 } else if declared.can(cx, Ability::Create) {
656 errors.push(form_error::<R>(DeclarationErrorKind::CreateWithoutForm));
657 }
658}
659
660fn check_form_inner<R: Resource>(
661 cx: &Cx,
662 declared: &Mounted<R>,
663 errors: &mut Vec<DeclarationError>,
664) {
665 let (fields, form) = (declared.fields.as_slice(), &*declared.form);
666 for field in fields {
668 for key in &field.keys {
669 if !form.fields().any(|control| control.name() == key) {
670 errors.push(form_error::<R>(DeclarationErrorKind::MissingControl {
671 field: field.name.to_string(),
672 key: key.clone(),
673 }));
674 }
675 }
676 }
677 if let Some(column) = tenant_column(declared)
679 && let Some(field) = fields.iter().find(|field| field.keys.contains(&column))
680 {
681 errors.push(form_error::<R>(
682 DeclarationErrorKind::FormClaimsTenantColumn {
683 field: field.name.to_string(),
684 column,
685 },
686 ));
687 }
688 for field in form.fields().filter(|field| {
689 field
690 .as_choice()
691 .is_some_and(|choice| choice.has_composite_source())
692 }) {
693 errors.push(form_error::<R>(DeclarationErrorKind::CompositeKeyChoice {
694 field: field.name().to_string(),
695 }));
696 }
697 for field in form.fields() {
698 if let Some(source) = field
699 .as_choice()
700 .and_then(|choice| choice.unavailable_source(cx))
701 {
702 errors.push(form_error::<R>(
703 DeclarationErrorKind::UnregisteredOptionSource {
704 field: field.name().to_string(),
705 source,
706 },
707 ));
708 }
709 }
710 if declared.tenancy.via_is_single() == Some(false) {
711 check_via_foreign_keys(cx, declared, errors);
712 }
713 check_many_to_many(cx, declared, errors);
714 if declared.can(cx, Ability::Create) {
715 check_create_columns(declared, errors);
716 }
717 let columns = model::fields::<R::Model>();
718 for field in form.fields().filter(|field| field.is_unique()) {
719 let name = field.name();
720 let backed = columns
721 .iter()
722 .any(|column| column.name == name && column.unique);
723 if !backed {
724 errors.push(form_error::<R>(DeclarationErrorKind::UniqueWithoutIndex {
725 field: name.to_string(),
726 }));
727 }
728 if !field.is_required() && !field.is_nullable() {
729 errors.push(form_error::<R>(DeclarationErrorKind::OptionalUnique {
730 field: name.to_string(),
731 }));
732 }
733 }
734}
735
736fn check_many_to_many<R: Resource>(
740 cx: &Cx,
741 declared: &Mounted<R>,
742 errors: &mut Vec<DeclarationError>,
743) {
744 let Some(schema) = AppSchema::of(cx) else {
745 return;
746 };
747 for field in declared.form.fields() {
748 let name = field.name();
749 let choice = field
750 .as_choice()
751 .filter(|choice| choice.is_multiple() && choice.is_relationship());
752 let fault = if model::is_via::<R::Model>(name) {
753 match (choice, JoinTable::of::<R::Model>(&schema, name)) {
754 (_, Err(fault)) => Some(fault),
755 (None, Ok(_)) => Some(ManyToManyFault::NotMultipleChoice),
756 (Some(choice), Ok(join)) => (choice.source_model() != Some(join.target_model()))
757 .then_some(ManyToManyFault::OtherSource),
758 }
759 } else {
760 field
761 .as_choice()
762 .is_some_and(|choice| choice.is_multiple())
763 .then_some(ManyToManyFault::NoJoin)
764 };
765 if let Some(fault) = fault {
766 errors.push(form_error::<R>(DeclarationErrorKind::ManyToMany {
767 field: name.to_string(),
768 fault,
769 }));
770 }
771 }
772}
773
774fn tenant_column<R: Resource>(declared: &Mounted<R>) -> Option<String> {
776 declared
777 .tenancy
778 .column_field()
779 .and_then(Result::ok)
780 .map(|field| field.name.clone())
781}
782
783fn check_create_columns<R: Resource>(declared: &Mounted<R>, errors: &mut Vec<DeclarationError>) {
785 let fields = &declared.fields;
786 let mut create_columns = Vec::new();
787 for column in &declared.create_columns {
788 match column {
789 Ok(name) => create_columns.push(name.as_str()),
790 Err(kind) => errors.push(DeclarationError::of::<R>(Site::Registration, kind.clone())),
791 }
792 }
793 let prefilled = model::prefilled_fields::<R::Model>();
794 let tenant = tenant_column(declared);
795 if let Some(column) = &tenant
796 && create_columns.contains(&column.as_str())
797 {
798 errors.push(DeclarationError::of::<R>(
799 Site::Registration,
800 DeclarationErrorKind::CreateColumnsNameTenant {
801 column: column.clone(),
802 },
803 ));
804 }
805 for field in &model::fields::<R::Model>() {
806 let name = field.name.as_str();
807 let filled = field.nullable
808 || field.relation
809 || prefilled.get(field.index).copied().unwrap_or(false)
810 || tenant.as_deref() == Some(name)
811 || fields.iter().any(|claim| claim.name == name)
812 || create_columns.contains(&name);
813 if !filled {
814 errors.push(form_error::<R>(DeclarationErrorKind::UnwrittenColumn {
815 column: name.to_string(),
816 }));
817 }
818 }
819}
820
821fn validation_cx(db: &Db, mounts: &Arc<Mounts>) -> Cx {
825 let mut app_context = topcoat::context::AppContext::new();
826 app_context.insert(db.clone());
827 app_context.insert(Arc::clone(mounts));
828 app_context.insert(MountScope(|cx| {
829 topcoat::context::try_app_context::<Arc<Mounts>>(cx).map(|mounts| &**mounts)
830 }));
831 Cx::new(Arc::new(app_context))
832}
833
834fn check_via_foreign_keys<R: Resource>(
838 cx: &Cx,
839 declared: &Mounted<R>,
840 errors: &mut Vec<DeclarationError>,
841) {
842 let form = &declared.form;
843 let Some(model::BelongsTo {
845 name: relation,
846 target: parent,
847 foreign_keys: keys,
848 }) = declared
849 .tenancy
850 .via_hop()
851 .and_then(model::belongs_to::<R::Model>)
852 else {
853 errors.push(DeclarationError::of::<R>(
854 Site::Tenancy,
855 DeclarationErrorKind::TenancyViaWithoutBelongsTo,
856 ));
857 return;
858 };
859 let unguarded: Vec<String> = keys
860 .into_iter()
861 .filter(|key| {
862 !form.fields().any(|field| {
863 field.name() == key
864 && field
865 .as_choice()
866 .and_then(|choice| choice.tenant_scoped_model(cx))
867 == Some(parent)
868 })
869 })
870 .collect();
871 if !unguarded.is_empty() {
872 errors.push(form_error::<R>(DeclarationErrorKind::UnguardedForeignKey {
873 relation,
874 keys: unguarded,
875 }));
876 }
877}
878
879fn served_root(path: &str) -> &str {
882 path.rsplit_once("{*").map_or(path, |(root, _)| root)
883}
884
885pub(crate) fn route_path(path: &str) -> topcoat::router::PathBuf {
887 Path::from_str(path)
888 .expect("panel route paths are well-formed")
889 .to_owned()
890}
891
892#[cfg(test)]
893mod tests;